Skip to content

Commit 7e3c098

Browse files
authored
Merge pull request #659 from xcp-ng/dnt/vif_configure
guest_tools/win: Add VifConfigureFeature tests
2 parents 504b0e9 + 7b0cdab commit 7e3c098

3 files changed

Lines changed: 308 additions & 17 deletions

File tree

lib/vif.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import annotations
22

33
import logging
4+
import time
45

56
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set
67

7-
from typing import TYPE_CHECKING
8+
from typing import TYPE_CHECKING, Literal, overload
89

910
if TYPE_CHECKING:
1011
from lib.vm import VM
@@ -64,3 +65,72 @@ def plug(self) -> None:
6465
def unplug(self, force: bool = False) -> None:
6566
logging.info("Unplugging VIF %s on VM %s", self.param_get('device'), self.vm.uuid)
6667
self.vm.host.xe('vif-unplug', {'uuid': self.uuid, 'force': force})
68+
69+
def _configure(
70+
self,
71+
address_family: str,
72+
mode: str,
73+
address: str | None = None,
74+
gateway: str | None = None,
75+
) -> None:
76+
args: dict[str, str | bool | dict[str, str]] = {"uuid": self.uuid, "mode": mode}
77+
if address is not None:
78+
args["address"] = address
79+
if gateway is not None:
80+
args["gateway"] = gateway
81+
self.vm.host.xe(f"vif-configure-{address_family}", args)
82+
# HACK: xe returns after publishing the request, before the guest has acknowledged it. Give the guest time to
83+
# consume it so a following request cannot overwrite the pending one.
84+
time.sleep(5)
85+
86+
@overload
87+
def configure_ipv4(
88+
self,
89+
mode: Literal["static"],
90+
address: str,
91+
gateway: str | None = None,
92+
) -> None: #
93+
...
94+
95+
@overload
96+
def configure_ipv4(
97+
self,
98+
mode: Literal["dhcp"] | Literal["none"],
99+
address: None = None,
100+
gateway: None = None,
101+
) -> None: #
102+
...
103+
104+
def configure_ipv4(
105+
self,
106+
mode: str,
107+
address: str | None = None,
108+
gateway: str | None = None,
109+
) -> None:
110+
self._configure("ipv4", mode, address, gateway)
111+
112+
@overload
113+
def configure_ipv6(
114+
self,
115+
mode: Literal["static"],
116+
address: str,
117+
gateway: str | None = None,
118+
) -> None: #
119+
...
120+
121+
@overload
122+
def configure_ipv6(
123+
self,
124+
mode: Literal["autoconf"] | Literal["none"],
125+
address: None = None,
126+
gateway: None = None,
127+
) -> None: #
128+
...
129+
130+
def configure_ipv6(
131+
self,
132+
mode: str,
133+
address: str | None = None,
134+
gateway: str | None = None,
135+
) -> None:
136+
self._configure("ipv6", mode, address, gateway)

lib/windows/__init__.py

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -128,37 +128,109 @@ def vif_get_mac_without_separator(vif: VIF) -> str:
128128
return mac.replace(":", "")
129129

130130

131-
def vif_has_rss(vif: VIF) -> bool:
132-
# Even if the Xenvif hash setting request fails, Windows can still report the NIC as having RSS enabled as long as
133-
# the relevant OIDs are supported (Get-NetAdapterRss reports Enabled as True and Profile as Default).
134-
# We need to explicitly check MaxProcessors to see if the hash setting request has really succeeded.
131+
def vif_exists(vif: VIF) -> bool:
135132
mac = vif_get_mac_without_separator(vif)
136133
return strtobool(
137134
vif.vm.execute_powershell_script(
138-
rf"""(Get-NetAdapter |
139-
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}} |
140-
Get-NetAdapterRss).MaxProcessors -gt 0"""
135+
rf"""$null -ne (Get-NetAdapter |
136+
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}})"""
141137
)
142138
)
143139

144140

145-
def vif_get_dns(vif: VIF) -> list[str]:
141+
def vif_execute_powershell_script(vif: VIF, script: str) -> str:
142+
"""Execute the given script with the matching adapter being stored in $adapter"""
146143
mac = vif_get_mac_without_separator(vif)
147144
return vif.vm.execute_powershell_script(
148-
rf"""Import-Module DnsClient; Get-NetAdapter |
149-
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}} |
145+
rf"""$adapter = Get-NetAdapter |
146+
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}};
147+
if ($null -eq $adapter) {{ throw 'Cannot find the VIF network adapter' }};
148+
{script}"""
149+
)
150+
151+
152+
def vif_has_rss(vif: VIF) -> bool:
153+
# Even if the Xenvif hash setting request fails, Windows can still report the NIC as having RSS enabled as long as
154+
# the relevant OIDs are supported (Get-NetAdapterRss reports Enabled as True and Profile as Default).
155+
# We need to explicitly check MaxProcessors to see if the hash setting request has really succeeded.
156+
return strtobool(vif_execute_powershell_script(vif, r"($adapter | Get-NetAdapterRss).MaxProcessors -gt 0"))
157+
158+
159+
def vif_get_dns(vif: VIF) -> list[str]:
160+
return vif_execute_powershell_script(
161+
vif,
162+
r"""Import-Module DnsClient;
163+
$adapter |
150164
Get-DnsClientServerAddress -AddressFamily IPv4 |
151-
Select-Object -ExpandProperty ServerAddresses"""
165+
Select-Object -ExpandProperty ServerAddresses""",
152166
).splitlines()
153167

154168

155169
def vif_set_dns(vif: VIF, nameservers: list[str]) -> None:
156-
mac = vif_get_mac_without_separator(vif)
157-
vif.vm.execute_powershell_script(
158-
rf"""Import-Module DnsClient; Get-NetAdapter |
159-
Where-Object {{$_.PnPDeviceID -notlike 'root\kdnic\*' -and $_.PermanentAddress -eq '{mac}'}} |
170+
vif_execute_powershell_script(
171+
vif,
172+
rf"""Import-Module DnsClient;
173+
$adapter |
160174
Get-DnsClientServerAddress -AddressFamily IPv4 |
161-
Set-DnsClientServerAddress -ServerAddresses {",".join(nameservers)}"""
175+
Set-DnsClientServerAddress -ServerAddresses {",".join(nameservers)}""",
176+
)
177+
178+
179+
def vif_has_static_configuration(
180+
vif: VIF,
181+
address_family: str,
182+
address: str,
183+
prefix: int,
184+
gateway: str,
185+
*,
186+
present: bool = True,
187+
) -> bool:
188+
default_route = "0.0.0.0/0" if address_family == "IPv4" else "::/0"
189+
comparison = "-gt 0" if present else "-eq 0"
190+
return strtobool(
191+
vif_execute_powershell_script(
192+
vif,
193+
rf"""$addresses = @(Get-NetIPAddress `
194+
-InterfaceIndex $adapter.ifIndex `
195+
-AddressFamily {address_family} `
196+
-ErrorAction SilentlyContinue |
197+
Where-Object {{$_.IPAddress -eq '{address}' -and $_.PrefixLength -eq {prefix} -and `
198+
$_.PrefixOrigin -eq 'Manual' -and $_.SuffixOrigin -eq 'Manual'}});
199+
200+
$gateways = @(Get-NetRoute `
201+
-InterfaceIndex $adapter.ifIndex `
202+
-AddressFamily {address_family} `
203+
-ErrorAction SilentlyContinue |
204+
Where-Object {{$_.DestinationPrefix -eq '{default_route}' -and $_.NextHop -eq '{gateway}'}});
205+
206+
($addresses.Count {comparison}) -and ($gateways.Count {comparison})""",
207+
)
208+
)
209+
210+
211+
def vif_uses_dhcp(vif: VIF) -> bool:
212+
return strtobool(
213+
vif_execute_powershell_script(
214+
vif,
215+
"(Get-NetIPInterface -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4).Dhcp -eq 'Enabled'",
216+
)
217+
)
218+
219+
220+
def vif_add_manual_configuration(
221+
vif: VIF,
222+
address: str,
223+
prefix: int,
224+
gateway: str,
225+
) -> None:
226+
vif_execute_powershell_script(
227+
vif,
228+
rf"""$null = New-NetIPAddress `
229+
-InterfaceIndex $adapter.ifIndex `
230+
-IPAddress '{address}' `
231+
-PrefixLength {prefix} `
232+
-DefaultGateway '{gateway}' `
233+
-ErrorAction Stop""",
162234
)
163235

164236

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import pytest
2+
3+
import logging
4+
5+
from lib.common import Defer, wait_for
6+
from lib.vif import VIF
7+
from lib.vm import VM
8+
from lib.windows import (
9+
vif_add_manual_configuration,
10+
vif_execute_powershell_script,
11+
vif_exists,
12+
vif_has_static_configuration,
13+
vif_uses_dhcp,
14+
)
15+
16+
from typing import Generator
17+
18+
# Requirements:
19+
# - Same as TestGuestToolsWindowsNondestructive.
20+
21+
22+
@pytest.mark.multi_vms
23+
@pytest.mark.usefixtures("windows_vm")
24+
class TestVifConfigure:
25+
@pytest.fixture
26+
def temporary_vif(self, vm_install_test_tools_per_test_class: VM, defer: Defer) -> Generator[VIF, None, None]:
27+
vm = vm_install_test_tools_per_test_class
28+
existing_vifs = vm.vifs()
29+
network_uuid = existing_vifs[0].param_get("network-uuid")
30+
assert network_uuid is not None
31+
32+
logging.info("Create temporary VIF")
33+
vif = vm.create_vif(1, network_uuid=network_uuid)
34+
defer(lambda: vif.destroy())
35+
vif.plug()
36+
defer(lambda: vif.unplug())
37+
38+
wait_for(lambda: vif_exists(vif), "Wait for temporary VIF network adapter")
39+
# A static default route on this test-only interface must never take precedence over the management VIF.
40+
vif_execute_powershell_script(
41+
vif,
42+
r"""Set-NetIPInterface -InterfaceIndex $adapter.ifIndex `
43+
-AddressFamily IPv4 `
44+
-AutomaticMetric Disabled `
45+
-InterfaceMetric 9999;
46+
Set-NetIPInterface -InterfaceIndex $adapter.ifIndex `
47+
-AddressFamily IPv6 `
48+
-AutomaticMetric Disabled `
49+
-InterfaceMetric 9999""",
50+
)
51+
yield vif
52+
53+
def test_vif_configure_ipv4(self, temporary_vif: VIF) -> None:
54+
vif = temporary_vif
55+
address1 = "192.0.2.2"
56+
prefix = 24
57+
gateway1 = "192.0.2.1"
58+
address2 = "198.51.100.2"
59+
gateway2 = "198.51.100.1"
60+
61+
logging.info("Configure DHCP IPv4")
62+
vif.configure_ipv4("dhcp")
63+
wait_for(lambda: vif_uses_dhcp(vif))
64+
65+
logging.info("Configure static IPv4")
66+
vif.configure_ipv4("static", f"{address1}/{prefix}", gateway1)
67+
wait_for(lambda: vif_has_static_configuration(vif, "IPv4", address1, prefix, gateway1))
68+
69+
logging.info("Configure noop IPv4")
70+
vif.configure_ipv4("none")
71+
wait_for(lambda: vif_has_static_configuration(vif, "IPv4", address1, prefix, gateway1))
72+
73+
logging.info("Reconfigure static IPv4")
74+
vif.configure_ipv4("static", f"{address2}/{prefix}", gateway2)
75+
wait_for(lambda: vif_has_static_configuration(vif, "IPv4", address1, prefix, gateway1, present=False))
76+
wait_for(lambda: vif_has_static_configuration(vif, "IPv4", address2, prefix, gateway2))
77+
78+
logging.info("Configure DHCP IPv4")
79+
vif.configure_ipv4("dhcp")
80+
wait_for(lambda: vif_uses_dhcp(vif))
81+
wait_for(lambda: vif_has_static_configuration(vif, "IPv4", address2, prefix, gateway2, present=False))
82+
83+
def test_vif_configure_ipv4_dhcp_to_static(self, temporary_vif: VIF) -> None:
84+
vif = temporary_vif
85+
address = "203.0.113.2"
86+
prefix = 24
87+
gateway = "203.0.113.1"
88+
89+
logging.info("Configure DHCP IPv4")
90+
vif.configure_ipv4("dhcp")
91+
wait_for(lambda: vif_uses_dhcp(vif))
92+
93+
logging.info("Add manual configuration")
94+
vif_add_manual_configuration(vif, address, prefix, gateway)
95+
wait_for(lambda: vif_has_static_configuration(vif, "IPv4", address, prefix, gateway))
96+
97+
logging.info("Configure static IPv4 while keeping address %s/%s and gateway %s", address, prefix, gateway)
98+
vif.configure_ipv4("static", f"{address}/{prefix}", gateway)
99+
wait_for(lambda: vif_has_static_configuration(vif, "IPv4", address, prefix, gateway))
100+
101+
def test_vif_configure_ipv6(self, temporary_vif: VIF) -> None:
102+
vif = temporary_vif
103+
address1 = "2001:db8:1::2"
104+
prefix = 64
105+
gateway1 = "2001:db8:1::1"
106+
address2 = "2001:db8:2::3"
107+
gateway2 = "2001:db8:2::254"
108+
109+
logging.info("Configure autoconf IPv6")
110+
vif.configure_ipv6("autoconf")
111+
112+
logging.info("Configure static IPv6")
113+
vif.configure_ipv6("static", f"{address1}/{prefix}", gateway1)
114+
wait_for(lambda: vif_has_static_configuration(vif, "IPv6", address1, prefix, gateway1))
115+
116+
logging.info("Reconfigure noop IPv6")
117+
vif.configure_ipv6("none")
118+
wait_for(lambda: vif_has_static_configuration(vif, "IPv6", address1, prefix, gateway1))
119+
120+
logging.info("Reconfigure static IPv6")
121+
vif.configure_ipv6("static", f"{address2}/{prefix}", gateway2)
122+
wait_for(lambda: vif_has_static_configuration(vif, "IPv6", address1, prefix, gateway1, present=False))
123+
wait_for(lambda: vif_has_static_configuration(vif, "IPv6", address2, prefix, gateway2))
124+
125+
logging.info("Configure autoconf IPv6")
126+
vif.configure_ipv6("autoconf")
127+
wait_for(lambda: vif_has_static_configuration(vif, "IPv6", address2, prefix, gateway2, present=False))
128+
129+
def test_vif_configure_ipv6_autoconf_to_static(self, temporary_vif: VIF) -> None:
130+
vif = temporary_vif
131+
address = "2001:db8:3::2"
132+
prefix = 64
133+
gateway = "2001:db8:3::1"
134+
135+
logging.info("Configure autoconf IPv6")
136+
vif.configure_ipv6("autoconf")
137+
138+
logging.info("Add manual configuration")
139+
vif_add_manual_configuration(vif, address, prefix, gateway)
140+
wait_for(lambda: vif_has_static_configuration(vif, "IPv6", address, prefix, gateway))
141+
142+
logging.info(
143+
"Configure static IPv6 while keeping address %s/%s and gateway %s",
144+
address,
145+
prefix,
146+
gateway,
147+
)
148+
vif.configure_ipv6("static", f"{address}/{prefix}", gateway)
149+
wait_for(lambda: vif_has_static_configuration(vif, "IPv6", address, prefix, gateway))

0 commit comments

Comments
 (0)