|
19 | 19 | # with this program; if not, see <http://www.gnu.org/licenses/>. |
20 | 20 |
|
21 | 21 | import asyncio |
22 | | -from typing import Optional |
| 22 | +from typing import Optional, Union |
23 | 23 |
|
24 | 24 | import qubes.config |
25 | 25 | import qubes.events |
@@ -87,6 +87,27 @@ def on_domain_loaded(self, event): # pylint: disable=unused-argument |
87 | 87 | if changes: |
88 | 88 | self.app.save() |
89 | 89 |
|
| 90 | + @qubes.events.handler("domain-pre-start") |
| 91 | + def __on_domain_pre_start(self, event, **kwargs): |
| 92 | + """Prevents startup for domain having a volume with disabled snapshots |
| 93 | + and a DispVM based on this volume started |
| 94 | + """ |
| 95 | + # pylint: disable=unused-argument |
| 96 | + volume_with_disabled_snapshots = False |
| 97 | + for vol in self.volumes.values(): |
| 98 | + volume_with_disabled_snapshots |= vol.snapshots_disabled |
| 99 | + |
| 100 | + if not volume_with_disabled_snapshots: |
| 101 | + return |
| 102 | + |
| 103 | + for vm in self.dispvms: |
| 104 | + if vm.is_running(): |
| 105 | + raise qubes.exc.QubesVMNotHaltedError(vm) |
| 106 | + |
| 107 | + @qubes.events.handler("domain-shutdown") |
| 108 | + async def on_dvmtemplate_domain_shutdown(self, _event, **_kwargs): |
| 109 | + await self.refresh_preload() |
| 110 | + |
90 | 111 | @qubes.events.handler("domain-feature-delete:preload-dispvm-max") |
91 | 112 | def on_feature_delete_preload_dispvm_max( |
92 | 113 | self, event, feature |
@@ -115,23 +136,6 @@ def on_feature_pre_set_preload_dispvm_max( |
115 | 136 | "Invalid preload-dispvm-max value: not a digit" |
116 | 137 | ) |
117 | 138 |
|
118 | | - @qubes.events.handler("domain-pre-start") |
119 | | - def __on_domain_pre_start(self, event, **kwargs): |
120 | | - """Prevents startup for domain having a volume with disabled snapshots |
121 | | - and a DispVM based on this volume started |
122 | | - """ |
123 | | - # pylint: disable=unused-argument |
124 | | - volume_with_disabled_snapshots = False |
125 | | - for vol in self.volumes.values(): |
126 | | - volume_with_disabled_snapshots |= vol.snapshots_disabled |
127 | | - |
128 | | - if not volume_with_disabled_snapshots: |
129 | | - return |
130 | | - |
131 | | - for vm in self.dispvms: |
132 | | - if vm.is_running(): |
133 | | - raise qubes.exc.QubesVMNotHaltedError(vm) |
134 | | - |
135 | 139 | @qubes.events.handler("domain-feature-set:preload-dispvm-max") |
136 | 140 | def on_feature_set_preload_dispvm_max( |
137 | 141 | self, event, feature, value, oldvalue=None |
@@ -249,22 +253,40 @@ def __on_property_set_template(self, event, name, newvalue, oldvalue=None): |
249 | 253 | "domain-preload-dispvm-start", |
250 | 254 | ) |
251 | 255 | async def on_domain_preload_dispvm_used( |
252 | | - self, event, **kwargs |
253 | | - ): # pylint: disable=unused-argument |
| 256 | + self, |
| 257 | + event: str, |
| 258 | + dispvm: Optional[qubes.vm.BaseVM] = None, |
| 259 | + reason: Optional[str] = None, |
| 260 | + delay: Union[int, float] = 0, |
| 261 | + **kwargs, # pylint: disable=unused-argument |
| 262 | + ) -> None: |
254 | 263 | """ |
255 | | - Preloads on vacancy and offloads on excess. If the event suffix is |
| 264 | + Offloads on excess and preload on vacancy. |
256 | 265 | ``autostart``, the preloaded list is emptied before preloading. |
257 | 266 |
|
258 | | - :param event: event which was fired |
| 267 | + :param str event: Event which was fired. Events have the prefix \ |
| 268 | + ``domain-preload-dispvm-``. If the suffix is ``autostart``, the \ |
| 269 | + preload list is emptied before attempting to preload. If the \ |
| 270 | + suffix is ``used`` or ``start``, tries to preload until it fills \ |
| 271 | + gaps. |
| 272 | + :param qubes.vm.dispvm.DispVM dispvm: Disposable that was used |
| 273 | + :param str reason: Why the event was fired |
| 274 | + :param float delay: Proceed only after sleeping that many seconds |
259 | 275 | """ |
| 276 | + assert isinstance(self, qubes.vm.BaseVM) |
260 | 277 | event = event.removeprefix("domain-preload-dispvm-") |
261 | 278 | event_log = "Received preload event '%s'" % str(event) |
262 | | - if event == "used": |
263 | | - event_log += " for dispvm '%s'" % str(kwargs.get("dispvm")) |
264 | | - if "reason" in kwargs: |
265 | | - event_log += " because %s" % str(kwargs.get("reason")) |
| 279 | + if event == "used" and dispvm: |
| 280 | + event_log += " for dispvm '%s'" % str(dispvm) |
| 281 | + if reason: |
| 282 | + event_log += " because %s" % str(reason) |
| 283 | + if delay: |
| 284 | + event_log += " with a delay of %f second(s)" % float(delay) |
266 | 285 | self.log.info(event_log) |
267 | 286 |
|
| 287 | + if delay: |
| 288 | + await asyncio.sleep(delay) |
| 289 | + |
268 | 290 | if event == "autostart": |
269 | 291 | self.remove_preload_excess(0) |
270 | 292 | elif not self.can_preload(): |
@@ -329,7 +351,7 @@ def get_feat_global_preload_max(self) -> Optional[int]: |
329 | 351 | def get_feat_preload_max(self, force_local=False) -> int: |
330 | 352 | """Get the ``preload-dispvm-max`` feature as an integer. |
331 | 353 |
|
332 | | - :param force_local: ignore global setting. |
| 354 | + :param bool force_local: ignore global setting. |
333 | 355 | """ |
334 | 356 | assert isinstance(self, qubes.vm.BaseVM) |
335 | 357 | feature = "preload-dispvm-max" |
@@ -370,10 +392,33 @@ def can_preload(self) -> bool: |
370 | 392 | return True |
371 | 393 | return False |
372 | 394 |
|
| 395 | + async def refresh_preload(self) -> None: |
| 396 | + assert isinstance(self, qubes.vm.BaseVM) |
| 397 | + outdated = [] |
| 398 | + for qube in self.dispvms: |
| 399 | + if not qube.is_preload or not any( |
| 400 | + vol.is_outdated() for vol in qube.volumes.values() |
| 401 | + ): |
| 402 | + continue |
| 403 | + outdated.append(qube) |
| 404 | + self.remove_preload_from_list([qube.name]) |
| 405 | + if outdated: |
| 406 | + tasks = [self.app.domains[qube].cleanup() for qube in outdated] |
| 407 | + # TODO: ben: cleanup and preload with delay? Delay can help user |
| 408 | + # expects to shutdown template and get a fresh disposable. Delay |
| 409 | + # won't help if max is on the low end, such as 1-2. |
| 410 | + asyncio.ensure_future(asyncio.gather(*tasks)) |
| 411 | + asyncio.ensure_future( |
| 412 | + self.fire_event_async( |
| 413 | + "domain-preload-dispvm-start", |
| 414 | + reason="of outdated volume(s)", |
| 415 | + ) |
| 416 | + ) |
| 417 | + |
373 | 418 | def remove_preload_from_list(self, disposables: list[str]) -> None: |
374 | 419 | """Removes list of preload qubes from the list. |
375 | 420 |
|
376 | | - :param disposables: disposable names to remove from the preloaded list. |
| 421 | + :param list[str] disposables: disposable names to remove from list. |
377 | 422 | """ |
378 | 423 | assert isinstance(self, qubes.vm.BaseVM) |
379 | 424 | old_preload = self.get_feat_preload() |
|
0 commit comments