Skip to content

Commit 38cc8bb

Browse files
committed
Redo netvm setup after unpause
Applying deferred netvm for preloaded disposables is handled separately to ensure that before a disposable is returned to the user, the networking is already set up. If the domain-unpaused event of the NetVMMixin kicks in before the preload is used, it is ignored by the "is_preload" attribute, if it kicks after, it is ignored by the absent "deferred-netvm-original" feature. Fixes: QubesOS/qubes-issues#10173 For: QubesOS/qubes-issues#1512
1 parent 8de4cd0 commit 38cc8bb

File tree

6 files changed

+218
-14
lines changed

6 files changed

+218
-14
lines changed

qubes/tests/integ/network.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,37 @@ def test_000_simple_networking(self):
191191
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
192192
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
193193

194+
def test_001_simple_networking_paused(self):
195+
"""
196+
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
197+
"""
198+
self.testvm1.netvm = None
199+
self.loop.run_until_complete(self.start_vm(self.testvm1))
200+
self.loop.run_until_complete(self.testvm1.pause())
201+
self.testvm1.netvm = self.testnetvm
202+
self.assertEqual(
203+
self.testvm1.features.get("deferred-netvm-original", None), ""
204+
)
205+
self.loop.run_until_complete(self.testvm1.unpause())
206+
self.assertEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
207+
self.assertEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
208+
self.assertEqual(
209+
self.testvm1.features.get("deferred-netvm-original", None), None
210+
)
211+
212+
self.loop.run_until_complete(self.testvm1.pause())
213+
self.testvm1.netvm = None
214+
self.assertEqual(
215+
self.testvm1.features.get("deferred-netvm-original", None),
216+
self.testnetvm.name,
217+
)
218+
self.loop.run_until_complete(self.testvm1.unpause())
219+
self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_ip), 0)
220+
self.assertNotEqual(self.run_cmd(self.testvm1, self.ping_name), 0)
221+
self.assertEqual(
222+
self.testvm1.features.get("deferred-netvm-original", None), None
223+
)
224+
194225
def test_010_simple_proxyvm(self):
195226
"""
196227
:type self: qubes.tests.SystemTestCase | VMNetworkingMixin
@@ -389,7 +420,7 @@ def test_030_firewallvm_firewall(self):
389420
# block all except ICMP
390421

391422
self.testvm1.firewall.rules = [
392-
(qubes.firewall.Rule(None, action="accept", proto="icmp"))
423+
qubes.firewall.Rule(None, action="accept", proto="icmp")
393424
]
394425
self.testvm1.firewall.save()
395426
# Ugly hack b/c there is no feedback when the rules are actually

qubes/tests/vm/mix/net.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,76 @@ def test_145_netvm_change(self):
159159
mock_detach.reset_mock()
160160
mock_attach.reset_mock()
161161

162+
def test_146_netvm_defer(self):
163+
vm = self.get_vm()
164+
self.setup_netvms(vm)
165+
with (
166+
patch("qubes.vm.qubesvm.QubesVM.is_running", lambda x: True),
167+
patch("qubes.vm.qubesvm.QubesVM.is_paused", lambda x: True),
168+
patch("qubes.vm.mix.net.NetVMMixin.attach_network") as mock_attach,
169+
patch("qubes.vm.mix.net.NetVMMixin.detach_network") as mock_detach,
170+
patch("qubes.vm.qubesvm.QubesVM.create_qdb_entries"),
171+
):
172+
173+
with self.subTest("changing netvm"):
174+
original_netvm = vm.netvm.name
175+
vm.netvm = self.netvm2
176+
mock_detach.assert_not_called()
177+
mock_attach.assert_not_called()
178+
self.assertEqual(
179+
vm.features.get("deferred-netvm-original", None),
180+
original_netvm,
181+
)
182+
with patch(
183+
"qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False
184+
):
185+
vm.apply_deferred_netvm()
186+
self.assertEqual(
187+
vm.features.get("deferred-netvm-original", None), None
188+
)
189+
mock_detach.assert_called()
190+
mock_attach.assert_called()
191+
mock_detach.reset_mock()
192+
mock_attach.reset_mock()
193+
194+
with self.subTest("setting netvm to none"):
195+
original_netvm = vm.netvm.name
196+
vm.netvm = None
197+
mock_detach.assert_not_called()
198+
mock_attach.assert_not_called()
199+
self.assertEqual(
200+
vm.features.get("deferred-netvm-original", None),
201+
original_netvm,
202+
)
203+
with patch(
204+
"qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False
205+
):
206+
vm.apply_deferred_netvm()
207+
self.assertEqual(
208+
vm.features.get("deferred-netvm-original", None), None
209+
)
210+
mock_detach.assert_not_called()
211+
mock_attach.assert_not_called()
212+
213+
with self.subTest("resetting netvm to default"):
214+
original_netvm = vm.netvm.name if vm.netvm else ""
215+
del vm.netvm
216+
mock_detach.assert_not_called()
217+
mock_attach.assert_not_called()
218+
self.assertEqual(
219+
vm.features.get("deferred-netvm-original", None),
220+
original_netvm,
221+
)
222+
with patch(
223+
"qubes.vm.qubesvm.QubesVM.is_paused", lambda x: False
224+
):
225+
vm.apply_deferred_netvm()
226+
self.assertEqual(
227+
vm.features.get("deferred-netvm-original", None), None
228+
)
229+
mock_detach.assert_not_called()
230+
mock_attach.assert_called_once()
231+
162232
def test_150_ip(self):
163233
vm = self.get_vm()
164234
self.setup_netvms(vm)

qubes/tests/vm/qubesvm.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,7 +1691,12 @@ def test_600_libvirt_xml_hvm_pcidev(self):
16911691
)
16921692
dom0 = self.get_vm(name="dom0", qid=0)
16931693
vm = self.get_vm(uuid=my_uuid)
1694-
vm.netvm = None
1694+
vm.paused = lambda x: False
1695+
with unittest.mock.patch(
1696+
"qubes.vm.qubesvm.QubesVM.is_paused"
1697+
) as mock_is_paused:
1698+
mock_is_paused.return_value = False
1699+
vm.netvm = None
16951700
vm.virt_mode = "hvm"
16961701
vm.kernel = None
16971702
# even with meminfo-writer enabled, should have memory==maxmem
@@ -1803,7 +1808,11 @@ def test_600_libvirt_xml_hvm_pcidev_s0ix(self):
18031808
dom0 = self.get_vm(name="dom0", qid=0)
18041809
dom0.features["suspend-s0ix"] = True
18051810
vm = self.get_vm(uuid=my_uuid)
1806-
vm.netvm = None
1811+
with unittest.mock.patch(
1812+
"qubes.vm.qubesvm.QubesVM.is_paused"
1813+
) as mock_is_paused:
1814+
mock_is_paused.return_value = False
1815+
vm.netvm = None
18071816
vm.virt_mode = "hvm"
18081817
vm.kernel = None
18091818
# even with meminfo-writer enabled, should have memory==maxmem

qubes/vm/dispvm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,13 +577,15 @@ def use_preload(self):
577577
self.log.info("Using preloaded qube")
578578
if not appvm.features.get("internal", None):
579579
del self.features["internal"]
580+
self.apply_deferred_netvm()
580581
self.preload_requested = None
581582
del self.features["preload-dispvm-in-progress"]
582583
else:
583584
# Happens when unpause/resume occurs without qube being requested.
584585
self.log.warning("Using a preloaded qube before requesting it")
585586
if not appvm.features.get("internal", None):
586587
del self.features["internal"]
588+
self.apply_deferred_netvm()
587589
appvm.remove_preload_from_list([self.name])
588590
self.features["preload-dispvm-in-progress"] = False
589591
self.app.save()

qubes/vm/mix/net.py

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@
2020
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
2121
#
2222

23-
""" This module contains the NetVMMixin """
23+
"""This module contains the NetVMMixin"""
2424
import ipaddress
2525
import os
2626
import re
2727

2828
import libvirt # pylint: disable=import-error
29+
import lxml.etree
2930
import qubes
3031
import qubes.config
3132
import qubes.events
@@ -341,6 +342,45 @@ def on_domain_started_net(self, event, **kwargs):
341342
except (qubes.exc.QubesException, libvirt.libvirtError):
342343
vm.log.warning("Cannot attach network", exc_info=1)
343344

345+
def apply_deferred_netvm(self):
346+
"""Apply deferred netvm changes in case qube could not apply it at the
347+
time it was requested."""
348+
deferred_from = self.features.get("deferred-netvm-original", None)
349+
if deferred_from is None:
350+
return
351+
oldvalue = None
352+
if deferred_from in self.app.domains:
353+
oldvalue = self.app.domains[deferred_from]
354+
if oldvalue:
355+
self.fire_event(
356+
"property-pre-set:netvm",
357+
pre_event=True,
358+
name="netvm",
359+
newvalue=self.netvm,
360+
oldvalue=oldvalue,
361+
)
362+
if self.netvm:
363+
self.fire_event(
364+
"property-set:netvm",
365+
name="netvm",
366+
newvalue=self.netvm,
367+
oldvalue=oldvalue,
368+
)
369+
del self.features["deferred-netvm-original"]
370+
371+
@qubes.events.handler("domain-unpaused")
372+
def on_domain_unpaused_net(
373+
self, event, **kwargs
374+
): # pylint: disable=unused-argument
375+
"""Check for deferred netvm changes in case qube was paused while
376+
changes happened."""
377+
if getattr(self, "is_preload", False):
378+
# Networking of preloaded disposable must be done when it is being
379+
# marked as used, so we guarantee that it finishes before the user
380+
# can use the disposable.
381+
return
382+
self.apply_deferred_netvm()
383+
344384
@qubes.events.handler("domain-pre-shutdown")
345385
def on_domain_pre_shutdown(self, event, force=False):
346386
"""Checks before NetVM shutdown if any connected domains are running.
@@ -373,6 +413,12 @@ def attach_network(self):
373413
self.netvm.start()
374414

375415
self.netvm.set_mapped_ip_info_for_vm(self)
416+
417+
if self.is_paused():
418+
self.log.warning(
419+
"Deferred attaching libvirt net device because qube is paused"
420+
)
421+
return
376422
self.libvirt_domain.attachDevice(
377423
self.app.env.get_template("libvirt/devices/net.xml").render(vm=self)
378424
)
@@ -383,12 +429,48 @@ def detach_network(self):
383429
if not self.is_running():
384430
raise qubes.exc.QubesVMNotRunningError(self)
385431
if self.netvm is None:
386-
raise qubes.exc.QubesVMError(
387-
self, "netvm should not be {}".format(self.netvm)
432+
deferred_from = self.features.get("deferred-netvm", None)
433+
if deferred_from is not None:
434+
raise qubes.exc.QubesVMError(
435+
self, "netvm should not be {}".format(self.netvm)
436+
)
437+
438+
if self.is_paused():
439+
self.log.warning(
440+
"Deferred detaching libvirt net device because qube is paused"
388441
)
442+
return
443+
444+
# Properties extracted from libvirt_domain to support deferred netvm.
445+
root = lxml.etree.fromstring(self.libvirt_domain.XMLDesc())
446+
interface = root.find(".//interface[@type='ethernet']")
447+
if interface is None:
448+
return
389449

450+
class NetVM: # pylint: disable=too-few-public-methods
451+
name = None
452+
453+
class VM: # pylint: disable=too-few-public-methods
454+
mac = ip = ip6 = None
455+
netvm = NetVM()
456+
457+
vm = VM()
458+
backend_elem = interface.find("backenddomain")
459+
if backend_elem is not None:
460+
netvm_name = backend_elem.get("name")
461+
if netvm_name not in self.app.domains:
462+
return
463+
vm.netvm.name = netvm_name
464+
mac_elem = interface.find("mac")
465+
if mac_elem is not None:
466+
vm.mac = mac_elem.get("address")
467+
for ip_elem in interface.findall("ip"):
468+
if ip_elem.get("family") == "ipv4":
469+
vm.ip = ip_elem.get("address")
470+
elif ip_elem.get("family") == "ipv6":
471+
vm.ip6 = ip_elem.get("address")
390472
self.libvirt_domain.detachDevice(
391-
self.app.env.get_template("libvirt/devices/net.xml").render(vm=self)
473+
self.app.env.get_template("libvirt/devices/net.xml").render(vm=vm)
392474
)
393475

394476
def is_networked(self):
@@ -515,18 +597,27 @@ def on_property_pre_set_netvm(self, event, name, newvalue, oldvalue=None):
515597
),
516598
)
517599

518-
# don't check oldvalue, because it's missing if it was default
519-
if self.netvm is not None:
520-
if self.is_running() and self.netvm.is_running():
521-
self.detach_network()
600+
if self.is_paused():
601+
if "deferred-netvm-original" not in self.features:
602+
self.features["deferred-netvm-original"] = (
603+
oldvalue.name if oldvalue else None
604+
)
605+
return
606+
deferred_from = self.features.get("deferred-netvm-original", None)
607+
if deferred_from is None:
608+
# don't check oldvalue, because it's missing if it was default
609+
if self.netvm is None:
610+
return
611+
if not (self.is_running() and self.netvm.is_running()):
612+
return
613+
self.detach_network()
522614

523615
@qubes.events.handler("property-set:netvm")
524616
def on_property_set_netvm(self, event, name, newvalue, oldvalue=None):
525617
"""Replaces the current NetVM with a new one and fires
526618
net-domain-connect event
527619
"""
528620
# pylint: disable=unused-argument
529-
530621
if oldvalue is not None and oldvalue.is_running():
531622
oldvalue.reload_connected_ips()
532623

@@ -539,7 +630,8 @@ def on_property_set_netvm(self, event, name, newvalue, oldvalue=None):
539630
if self.is_running():
540631
# refresh IP, DNS etc
541632
self.create_qdb_entries()
542-
self.attach_network()
633+
if not self.is_paused():
634+
self.attach_network()
543635

544636
newvalue.fire_event("net-domain-connect", vm=self)
545637

templates/libvirt/devices/net.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
<script path="vif-route-qubes" />
99
</interface>
1010
11-
{# vim : set ft=jinja ts=4 sts=4 sw=4 et : #}
11+
{# vim: set ft=jinja ts=4 sts=4 sw=4 et : #}

0 commit comments

Comments
 (0)