Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ all:

install:
ifeq ($(OS),Linux)
$(MAKE) install -C linux/autostart
$(MAKE) install -C linux/systemd
$(MAKE) install -C linux/aux-tools
$(MAKE) install -C linux/system-config
Expand Down
6 changes: 6 additions & 0 deletions linux/autostart/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
all:
true

install:
mkdir -p $(DESTDIR)/etc/xdg/autostart
cp qubes-preload-dispvm-gui.desktop $(DESTDIR)/etc/xdg/autostart
9 changes: 9 additions & 0 deletions linux/autostart/qubes-preload-dispvm-gui.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Desktop Entry]
Icon=qubes
Name=Qubes Preload Disposables
Comment=Workaround for session monitoring with qubes.WaitForSession
Categories=System;Monitor;
Exec=systemctl start qubes-preload-dispvm-gui.service
Terminal=false
NoDisplay=true
Type=Application
17 changes: 15 additions & 2 deletions linux/aux-tools/preload-dispvm
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
"""
This script is outside of qubesd because it relies on systemd to:

- Order this action after the autostart or standard qubes;
- Order this action after the autostart of normal qubes;
- Skip preloading if kernel command line prevents autostart.
"""

import argparse
import asyncio
import concurrent.futures
import qubesadmin
Expand All @@ -23,6 +24,18 @@ def get_preload_max(qube) -> int | None:


async def main():
parser = argparse.ArgumentParser(
description="Autostart preloaded disposable cycle"
)
parser.add_argument(
"--gui", action="store_true", help="start qubes with GUI session"
)
args = parser.parse_args()

call_arg = "preload-autostart"
if args.gui:
call_arg += "+gui"

app = qubesadmin.Qubes()
domains = app.domains
default_dispvm = getattr(app, "default_dispvm", None)
Expand Down Expand Up @@ -54,7 +67,7 @@ async def main():
maximum = get_preload_max(qube)
msg = f"{qube}:{maximum}"
print(repr(msg))
exec_args = qube.qubesd_call, qube.name, method, "preload-autostart"
exec_args = qube.qubesd_call, qube.name, method, call_arg
future = loop.run_in_executor(executor, *exec_args)
tasks.append(future)
await asyncio.gather(*tasks)
Expand Down
1 change: 1 addition & 0 deletions linux/systemd/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ install:
cp qubes-qmemman.service $(DESTDIR)$(UNITDIR)
cp qubesd.service $(DESTDIR)$(UNITDIR)
cp qubes-preload-dispvm.service $(DESTDIR)$(UNITDIR)
cp qubes-preload-dispvm-gui.service $(DESTDIR)$(UNITDIR)
install -d $(DESTDIR)$(UNITDIR)/lvm2-pvscan@.service.d
install -m 0644 lvm2-pvscan@.service.d_30_qubes.conf \
$(DESTDIR)$(UNITDIR)/lvm2-pvscan@.service.d/30_qubes.conf
14 changes: 14 additions & 0 deletions linux/systemd/qubes-preload-dispvm-gui.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Preload Qubes' Disposables after DISPLAY is available
ConditionKernelCommandLine=!qubes.skip_autostart
# After qmemman so the daemon can create the file containing available memory.
After=qubesd.service qubes-meminfo-writer-dom0.service

[Service]
Type=oneshot
ExecStart=/usr/lib/qubes/preload-dispvm --gui
Group=qubes
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
2 changes: 1 addition & 1 deletion linux/systemd/qubes-preload-dispvm.service
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[Unit]
Description=Preload Qubes DispVMs
Description=Preload Qubes' Disposables
ConditionKernelCommandLine=!qubes.skip_autostart
# After qmemman so the daemon can create the file containing available memory.
After=qubesd.service qubes-meminfo-writer-dom0.service
Expand Down
21 changes: 17 additions & 4 deletions qubes/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1308,13 +1308,20 @@ async def _vm_create(
@qubes.api.method("admin.vm.CreateDisposable", scope="global", write=True)
async def create_disposable(self, untrusted_payload):
"""
Create a disposable. If the RPC argument is ``preload-autostart``,
cleanse the preload list and start preloading fresh disposables.
Create a disposable. If the RPC argument starts with
``preload-autostart``, cleanse the preload list and start preloading
fresh disposables.
"""
self.enforce(self.arg in [None, "", "preload-autostart"])
self.enforce(
self.arg in [None, "", "preload-autostart", "preload-autostart+gui"]
)
preload_autostart = False
preload_autostart_gui = False
if self.arg == "preload-autostart":
preload_autostart = True
elif self.arg == "preload-autostart+gui":
preload_autostart = True
preload_autostart_gui = True
if untrusted_payload not in (b"", b"uuid"):
raise qubes.exc.QubesValueError(
"Invalid payload for admin.vm.CreateDisposable: "
Expand All @@ -1327,9 +1334,15 @@ async def create_disposable(self, untrusted_payload):
appvm = self.dest

self.fire_event_for_permission(dispvm_template=appvm)

preload_autostart_event = None
if preload_autostart:
await appvm.fire_event_async("domain-preload-dispvm-autostart")
preload_autostart_event = "domain-preload-dispvm-autostart"
if preload_autostart_gui:
preload_autostart_event += "-gui"
await appvm.fire_event_async(preload_autostart_event)
return

dispvm = await qubes.vm.dispvm.DispVM.from_appvm(appvm)
# TODO: move this to extension (in race-free fashion, better than here)
dispvm.tags.add("created-by-" + str(self.src))
Expand Down
23 changes: 21 additions & 2 deletions qubes/vm/dispvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ class DispVM(qubes.vm.qubesvm.QubesVM):
}

def __init__(self, app, xml, *args, **kwargs) -> None:
assert isinstance(self, qubes.vm.BaseVM)
assert isinstance(self, qubes.vm.qubesvm.QubesVM)
self.volume_config = copy.deepcopy(self.default_volume_config)
template = kwargs.get("template", None)
self.preload_complete = asyncio.Event()
Expand Down Expand Up @@ -370,6 +370,7 @@ def __init__(self, app, xml, *args, **kwargs) -> None:
(key, value)
for key, value in template.features.items()
if not key.startswith("preload-dispvm")
and key != "preload-dispvm-early-gui"
]
)
self.tags.update(template.tags)
Expand Down Expand Up @@ -440,7 +441,7 @@ async def on_domain_started_dispvm(
return
timeout = self.qrexec_timeout
# https://github.com/QubesOS/qubes-issues/issues/9964
rpc = "qubes.WaitForRunningSystem"
rpc = self.get_preload_service(self)
path = "/run/qubes-rpc:/usr/local/etc/qubes-rpc:/etc/qubes-rpc"
service = '$(PATH="' + path + '" command -v ' + rpc + ")"
try:
Expand Down Expand Up @@ -844,3 +845,21 @@ async def start(self, **kwargs):
def create_qdb_entries(self) -> None:
super().create_qdb_entries()
self.untrusted_qdb.write("/qubes-vm-persistence", "none")

@staticmethod
def get_preload_service(obj) -> str:
"""
Check which service is requires to check if system is ready.

:rtype: str
"""
# pylint: disable=unused-argument
if (
obj.features.get("preload-dispvm-early-gui", None)
and obj.guivm
and obj.features.get("gui", True)
):
service = "qubes.WaitForSession"
else:
service = "qubes.WaitForRunningSystem"
return service
36 changes: 29 additions & 7 deletions qubes/vm/mix/dvmtemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def on_domain_loaded(self, event) -> None:
# pylint: disable=unused-argument
assert isinstance(self, qubes.vm.BaseVM)
changes = False
# Preloading began and host rebooted and autostart event didn't run yet.
# Began preloading, host rebooted and autostart event didn't run since.
old_preload = self.get_feat_preload()
clean_preload = old_preload.copy()
for unwanted_disp in old_preload:
Expand Down Expand Up @@ -180,7 +180,7 @@ def on_feature_pre_set_preload_dispvm_max(
if not self.features.check_with_template("qrexec", None):
raise qubes.exc.QubesValueError("Qube does not support qrexec")

service = "qubes.WaitForRunningSystem"
service = self.get_preload_service()
if not self.supports_preload():
raise qubes.exc.QubesValueError(
"Qube does not support the RPC '%s'" % service
Expand Down Expand Up @@ -412,6 +412,7 @@ def __on_property_set_template(
@qubes.events.handler(
"domain-preload-dispvm-used",
"domain-preload-dispvm-autostart",
"domain-preload-dispvm-autostart-gui",
"domain-preload-dispvm-start",
)
async def on_domain_preload_dispvm_used(
Expand Down Expand Up @@ -445,16 +446,30 @@ async def on_domain_preload_dispvm_used(
if delay:
event_log += " with a delay of %s second(s)" % f"{delay:.1f}"
self.log.info(event_log)
service = "qubes.WaitForRunningSystem"

if not self.supports_preload():
raise qubes.exc.QubesValueError(
"Qube does not support the RPC '%s' but tried to preload, "
"check if template is outdated" % service
"check if template is outdated" % self.get_preload_service()
)
if event.startswith("autostart"):
early_gui = self.features.get("preload-dispvm-early-gui", None)
if event == "autostart" and early_gui:
self.log.info(
"Skipping preload autostart as early GUI was requested"
)
return
if event == "autostart-gui" and not early_gui:
self.log.info(
"Skipping early GUI preload autostart as it was not "
"configured"
)
return

if delay:
await asyncio.sleep(delay)

if event == "autostart":
if event.startswith("autostart"):
self.remove_preload_excess(0, reason="event autostart was called")
elif not self.can_preload():
self.remove_preload_excess(reason="there may be absent qubes")
Expand Down Expand Up @@ -672,15 +687,22 @@ def remove_preload_excess(
dispvm = self.app.domains[unwanted_disp]
asyncio.ensure_future(dispvm.cleanup())

def get_preload_service(self) -> str:
"""
Check which service is requires to check if system is ready.

:rtype: str
"""
return qubes.vm.dispvm.DispVM.get_preload_service(self)

def supports_preload(self) -> bool:
"""
Check if the necessary RPC is supported.

:rtype: bool
"""
assert isinstance(self, qubes.vm.BaseVM)
service = "qubes.WaitForRunningSystem"
supported_service = "supported-rpc." + service
supported_service = "supported-rpc." + self.get_preload_service()
if self.features.check_with_template(supported_service, False):
return True
return False
3 changes: 3 additions & 0 deletions rpm_spec/core-dom0.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ done

%{_mandir}/man1/qubes*.1*

%{_sysconfdir}/xdg/autostart/qubes-preload-dispvm-gui.desktop

%dir %{python3_sitelib}/qubes-*.egg-info
%{python3_sitelib}/qubes-*.egg-info/*

Expand Down Expand Up @@ -578,6 +580,7 @@ done
%{_unitdir}/qubes-vm@.service
%{_unitdir}/qubesd.service
%{_unitdir}/qubes-preload-dispvm.service
%{_unitdir}/qubes-preload-dispvm-gui.service
%attr(2770,root,qubes) %dir /var/lib/qubes
%attr(2770,root,qubes) %dir /var/lib/qubes/vm-templates
%attr(2770,root,qubes) %dir /var/lib/qubes/appvms
Expand Down