Skip to content

Commit 7e18879

Browse files
committed
Set deferred netvm for paused clients on shutdown
The force feature exists for the sole reason of skipping deletion of the original feature if it has the same value as the current netvm. For: #722 Fixes: QubesOS/qubes-issues#10173 For: QubesOS/qubes-issues#1512
1 parent 6b978a3 commit 7e18879

6 files changed

Lines changed: 194 additions & 22 deletions

File tree

qubes/ext/admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030

3131
PROHIBITED_FEATURES = [
32+
"deferred-netvm-force",
3233
"deferred-netvm-original",
3334
"preload-dispvm",
3435
"preload-dispvm-completed",

qubes/tests/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,10 +1404,10 @@ def enter_keys_in_window(self, title, keys):
14041404
] + keys
14051405
subprocess.check_call(command)
14061406

1407-
def shutdown_and_wait(self, vm, timeout=60):
1407+
def shutdown_and_wait(self, vm, force=False, timeout=60):
14081408
try:
14091409
self.loop.run_until_complete(
1410-
vm.shutdown(wait=True, timeout=timeout)
1410+
vm.shutdown(wait=True, force=force, timeout=timeout)
14111411
)
14121412
except qubes.exc.QubesException:
14131413
name = vm.name

qubes/tests/integ/network.py

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ def _networking_paused_from_none_to_existent(
241241
self.assertEqual(
242242
self.testvm1.features.get("deferred-netvm-original", None), ""
243243
)
244+
self.assertIsNone(
245+
self.testvm1.features.get("deferred-netvm-force", None)
246+
)
244247
self.loop.run_until_complete(self.testvm1.unpause())
245248
self._run_cmd_and_log_output(self.testvm1, ip_deadline)
246249
self._run_cmd_and_log_output(self.testvm1, name_deadline)
@@ -254,8 +257,11 @@ def _networking_paused_from_none_to_existent(
254257
0,
255258
"Ping by name on " + test + " failed",
256259
)
257-
self.assertEqual(
258-
self.testvm1.features.get("deferred-netvm-original", None), None
260+
self.assertIsNone(
261+
self.testvm1.features.get("deferred-netvm-original", None)
262+
)
263+
self.assertIsNone(
264+
self.testvm1.features.get("deferred-netvm-force", None)
259265
)
260266
self.shutdown_and_wait(self.testvm1)
261267

@@ -273,6 +279,9 @@ def _networking_paused_from_existent_to_none(
273279
self.testvm1.features.get("deferred-netvm-original", None),
274280
self.testnetvm.name,
275281
)
282+
self.assertIsNone(
283+
self.testvm1.features.get("deferred-netvm-force", None)
284+
)
276285
self.loop.run_until_complete(self.testvm1.unpause())
277286
self.assertNotEqual(
278287
self.run_cmd(self.testvm1, ip),
@@ -284,8 +293,11 @@ def _networking_paused_from_existent_to_none(
284293
0,
285294
"Ping by name on " + test + " succeeded but should have failed",
286295
)
287-
self.assertEqual(
288-
self.testvm1.features.get("deferred-netvm-original", None), None
296+
self.assertIsNone(
297+
self.testvm1.features.get("deferred-netvm-original", None)
298+
)
299+
self.assertIsNone(
300+
self.testvm1.features.get("deferred-netvm-force", None)
289301
)
290302
self.shutdown_and_wait(self.testvm1)
291303

@@ -303,6 +315,9 @@ def _networking_paused_change_shutdown_old(
303315
self.testvm1.features.get("deferred-netvm-original", None),
304316
self.testnetvm2.name,
305317
)
318+
self.assertIsNone(
319+
self.testvm1.features.get("deferred-netvm-force", None)
320+
)
306321
self.shutdown_and_wait(self.testnetvm2)
307322
self.loop.run_until_complete(self.testvm1.unpause())
308323
self._run_cmd_and_log_output(self.testvm1, ip_deadline)
@@ -317,8 +332,11 @@ def _networking_paused_change_shutdown_old(
317332
0,
318333
"Ping by name on " + test + " failed",
319334
)
320-
self.assertEqual(
321-
self.testvm1.features.get("deferred-netvm-original", None), None
335+
self.assertIsNone(
336+
self.testvm1.features.get("deferred-netvm-original", None)
337+
)
338+
self.assertIsNone(
339+
self.testvm1.features.get("deferred-netvm-force", None)
322340
)
323341
self.shutdown_and_wait(self.testvm1)
324342

@@ -337,6 +355,9 @@ def _networking_paused_change_purge_old(
337355
self.testvm1.features.get("deferred-netvm-original", None),
338356
self.testnetvm2.name,
339357
)
358+
self.assertIsNone(
359+
self.testvm1.features.get("deferred-netvm-force", None)
360+
)
340361
self.loop.run_until_complete(self.testnetvm2.kill())
341362
del self.app.domains[self.testnetvm2]
342363
self.loop.run_until_complete(self.testnetvm2.remove_from_disk())
@@ -355,8 +376,84 @@ def _networking_paused_change_purge_old(
355376
0,
356377
"Ping by name on " + test + " failed",
357378
)
379+
self.assertIsNone(
380+
self.testvm1.features.get("deferred-netvm-original", None)
381+
)
382+
self.assertIsNone(
383+
self.testvm1.features.get("deferred-netvm-force", None)
384+
)
385+
self.shutdown_and_wait(self.testvm1)
386+
387+
def _networking_paused_restart_netvm(
388+
self, ip, name, ip_deadline, name_deadline
389+
):
390+
test = "restart netvm and unpause client"
391+
self.log.critical(test)
392+
print(test)
393+
self.testvm1.netvm = self.testnetvm
394+
self.loop.run_until_complete(self.start_vm(self.testvm1))
395+
self.loop.run_until_complete(self.testvm1.pause())
396+
self.shutdown_and_wait(self.testnetvm, force=True)
397+
self.assertEqual(
398+
self.testvm1.features.get("deferred-netvm-original", None),
399+
self.testnetvm.name,
400+
)
401+
self.assertTrue(self.testvm1.features.get("deferred-netvm-force", None))
402+
self.loop.run_until_complete(self.start_vm(self.testnetvm))
403+
self.loop.run_until_complete(self.testvm1.unpause())
404+
self._run_cmd_and_log_output(self.testvm1, ip_deadline)
405+
self._run_cmd_and_log_output(self.testvm1, name_deadline)
406+
self.assertEqual(
407+
self.run_cmd(self.testvm1, ip),
408+
0,
409+
"Ping by IP on " + test + " failed",
410+
)
411+
self.assertEqual(
412+
self.run_cmd(self.testvm1, name),
413+
0,
414+
"Ping by name on " + test + " failed",
415+
)
416+
self.assertIsNone(
417+
self.testvm1.features.get("deferred-netvm-original", None)
418+
)
419+
self.assertIsNone(
420+
self.testvm1.features.get("deferred-netvm-force", None)
421+
)
422+
self.shutdown_and_wait(self.testvm1)
423+
424+
def _networking_paused_shutdown_netvm(
425+
self, ip, name, ip_deadline, name_deadline
426+
):
427+
test = "shutdown netvm and unpause client"
428+
self.log.critical(test)
429+
print(test)
430+
self.testvm1.netvm = self.testnetvm
431+
self.loop.run_until_complete(self.start_vm(self.testvm1))
432+
self.loop.run_until_complete(self.testvm1.pause())
433+
self.shutdown_and_wait(self.testnetvm, force=True)
434+
self.assertEqual(
435+
self.testvm1.features.get("deferred-netvm-original", None),
436+
self.testnetvm.name,
437+
)
438+
self.assertTrue(self.testvm1.features.get("deferred-netvm-force", None))
439+
self.loop.run_until_complete(self.testvm1.unpause())
440+
self._run_cmd_and_log_output(self.testvm1, ip_deadline)
441+
self._run_cmd_and_log_output(self.testvm1, name_deadline)
442+
self.assertEqual(
443+
self.run_cmd(self.testvm1, ip),
444+
0,
445+
"Ping by IP on " + test + " failed",
446+
)
358447
self.assertEqual(
359-
self.testvm1.features.get("deferred-netvm-original", None), None
448+
self.run_cmd(self.testvm1, name),
449+
0,
450+
"Ping by name on " + test + " failed",
451+
)
452+
self.assertIsNone(
453+
self.testvm1.features.get("deferred-netvm-original", None)
454+
)
455+
self.assertIsNone(
456+
self.testvm1.features.get("deferred-netvm-force", None)
360457
)
361458
self.shutdown_and_wait(self.testvm1)
362459

@@ -406,6 +503,28 @@ def test_001_simple_networking_paused_change_purge_old(self):
406503
self.ping_deadline_name,
407504
)
408505

506+
def test_001_simple_networking_paused_restart_netvm(self):
507+
"""
508+
:type self: qubes.tests.SystemTestCase | VmNetworkingMixin
509+
"""
510+
self._networking_paused_restart_netvm(
511+
self.ping_ip,
512+
self.ping_name,
513+
self.ping_deadline_ip,
514+
self.ping_deadline_name,
515+
)
516+
517+
def test_001_simple_networking_paused_shutdown_netvm(self):
518+
"""
519+
:type self: qubes.tests.SystemTestCase | VmNetworkingMixin
520+
"""
521+
self._networking_paused_shutdown_netvm(
522+
self.ping_ip,
523+
self.ping_name,
524+
self.ping_deadline_ip,
525+
self.ping_deadline_name,
526+
)
527+
409528
def test_010_simple_proxyvm(self):
410529
"""
411530
:type self: qubes.tests.SystemTestCase | VmNetworkingMixin

qubes/tests/integ/network_ipv6.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,28 @@ def test_501_simple_networking_paused_change_purge_old(self):
167167
self.ping6_deadline_name,
168168
)
169169

170+
def test_501_simple_networking_paused_restart_netvm(self):
171+
"""
172+
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
173+
"""
174+
self._networking_paused_restart_netvm(
175+
self.ping6_ip,
176+
self.ping6_name,
177+
self.ping6_deadline_ip,
178+
self.ping6_deadline_name,
179+
)
180+
181+
def test_501_simple_networking_paused_shutdown_netvm(self):
182+
"""
183+
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin
184+
"""
185+
self._networking_paused_shutdown_netvm(
186+
self.ping6_ip,
187+
self.ping6_name,
188+
self.ping6_deadline_ip,
189+
self.ping6_deadline_name,
190+
)
191+
170192
def test_510_ipv6_simple_proxyvm(self):
171193
"""
172194
:type self: qubes.tests.SystemTestCase | VmIPv6NetworkingMixin

qubes/vm/dispvm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -749,18 +749,18 @@ async def use_preload(self) -> None:
749749
self.log.info("Using preloaded qube")
750750
if not appvm.features.get("internal", None):
751751
del self.features["internal"]
752-
await self.apply_deferred_netvm()
753752
self.preload_requested = False
753+
await self.apply_deferred_netvm()
754754
del self.features["preload-dispvm-in-progress"]
755755
else:
756756
# Happens when unpause/resume occurs without qube being requested.
757757
self.log.warning("Using a preloaded qube before requesting it")
758758
if not appvm.features.get("internal", None):
759759
del self.features["internal"]
760-
await self.apply_deferred_netvm()
761760
appvm.remove_preload_from_list(
762761
[self.name], reason="qube was used without being requested"
763762
)
763+
await self.apply_deferred_netvm()
764764
self.features["preload-dispvm-in-progress"] = False
765765
self.app.save()
766766
asyncio.ensure_future(

qubes/vm/mix/net.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -315,17 +315,24 @@ def on_domain_shutdown_net(self, event, **kwargs):
315315
This will allow re-reconnecting them cleanly later.
316316
"""
317317
# pylint: disable=unused-argument
318+
for feature in ["deferred-netvm-original", "deferred-netvm-force"]:
319+
deferred_feat = self.features.get(feature, None)
320+
if deferred_feat is None:
321+
continue
322+
del self.features[feature]
318323
for vm in self.connected_vms:
319324
if not vm.is_running():
320325
continue
321326
try:
322327
vm.detach_network()
328+
if vm.is_paused():
329+
vm.features["deferred-netvm-force"] = True
323330
except (qubes.exc.QubesException, libvirt.libvirtError):
324331
# ignore errors
325332
pass
326333

327334
@qubes.events.handler("domain-start")
328-
def on_domain_started_net(self, event, **kwargs):
335+
async def on_domain_started_net(self, event, **kwargs):
329336
"""Connect this domain to its downstream domains. Also reload firewall
330337
in its netvm.
331338
@@ -336,11 +343,15 @@ def on_domain_started_net(self, event, **kwargs):
336343
self.netvm.reload_firewall_for_vm(self) # pylint: disable=no-member
337344

338345
for vm in self.connected_vms:
339-
if not vm.is_running():
346+
if not vm.is_running() or vm.is_paused():
340347
continue
341348
vm.log.info("Attaching network")
342349
try:
343-
vm.attach_network()
350+
deferred_from = vm.features.get("deferred-netvm-original", None)
351+
if deferred_from is not None:
352+
await vm.apply_deferred_netvm()
353+
else:
354+
vm.attach_network()
344355
except (qubes.exc.QubesException, libvirt.libvirtError):
345356
vm.log.warning("Cannot attach network", exc_info=1)
346357

@@ -358,6 +369,11 @@ async def apply_deferred_netvm(self):
358369
oldvalue = None
359370
if deferred_from in self.app.domains:
360371
oldvalue = self.app.domains[deferred_from]
372+
if self.netvm and not self.netvm.is_running():
373+
# Too early to create routing table. Happens when shutting down
374+
# netvm and not starting it before attempting to use the client.
375+
# This function will be called again by domain-start of the netvm.
376+
return
361377
self.fire_event(
362378
"property-pre-set:netvm",
363379
pre_event=True,
@@ -373,6 +389,9 @@ async def apply_deferred_netvm(self):
373389
oldvalue=oldvalue,
374390
)
375391
del self.features["deferred-netvm-original"]
392+
deferred_force = self.features.get("deferred-netvm-force", None)
393+
if deferred_force is not None:
394+
del self.features["deferred-netvm-force"]
376395
# Guarantee that uplink has been concluded (established or absent).
377396
await asyncio.wait_for(
378397
self.run_service_for_stdio(
@@ -442,22 +461,32 @@ def detach_network(self):
442461

443462
if not self.is_running():
444463
raise qubes.exc.QubesVMNotRunningError(self)
464+
deferred_from = self.features.get("deferred-netvm", None)
445465
if self.netvm is None:
446-
deferred_from = self.features.get("deferred-netvm", None)
447466
if deferred_from is not None:
448467
raise qubes.exc.QubesVMError(
449468
self, "netvm should not be {}".format(self.netvm)
450469
)
451470

471+
# Properties extracted from libvirt_domain to support deferred netvm.
472+
root = lxml.etree.fromstring(self.libvirt_domain.XMLDesc())
473+
eth = root.find(".//interface[@type='ethernet']")
474+
452475
if self.is_paused():
453476
self.log.warning(
454477
"Deferred detaching libvirt net device because qube is paused"
455478
)
479+
if deferred_from is not None or eth is None:
480+
return
481+
backend = eth.find("backenddomain")
482+
if backend is None:
483+
return
484+
backend_name = backend.get("name")
485+
if backend_name not in self.app.domains:
486+
return
487+
self.features["deferred-netvm-original"] = str(backend_name)
456488
return
457489

458-
# Properties extracted from libvirt_domain to support deferred netvm.
459-
root = lxml.etree.fromstring(self.libvirt_domain.XMLDesc())
460-
eth = root.find(".//interface[@type='ethernet']")
461490
if eth is None:
462491
return
463492
self.libvirt_domain.detachDevice(lxml.etree.tostring(eth).decode())
@@ -587,10 +616,11 @@ def on_property_pre_set_netvm(self, event, name, newvalue, oldvalue=None):
587616
)
588617

589618
deferred_from = self.features.get("deferred-netvm-original", None)
590-
if deferred_from is not None and (
591-
(not deferred_from and not newvalue)
592-
or (newvalue and deferred_from == newvalue.name)
593-
):
619+
deferred_force = self.features.get("deferred-netvm-force", None)
620+
same_name = (not deferred_from and not newvalue) or (
621+
newvalue and deferred_from == newvalue.name
622+
)
623+
if not deferred_force and deferred_from is not None and same_name:
594624
# No deferred netvm as original value is restored.
595625
del self.features["deferred-netvm-original"]
596626
return

0 commit comments

Comments
 (0)