Skip to content

Commit 15ff2e8

Browse files
committed
Add traffic rules tests
tests: - simple VIF rule (add/delete with simple vm.start/destroy cycle) - simple Network rule (add/delete with simple vm.start/destroy cycle) - migrate with simple VIF rule - migrate with Network rule rule - VLAN with simple VIF rule - VLAN with simple Network rule - Tunnel with simple VIF rule - Tunnel with simple Network rule fixtures: - add Tunnel fixture: returns a configured Tunnel network (Private Network in XO) - add VLAN fixture: returns a configured VLAN network Signed-off-by: Sebastien Rodot <sebastien.rodot@vates.tech>
1 parent 3a40efb commit 15ff2e8

3 files changed

Lines changed: 1003 additions & 6 deletions

File tree

jobs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class JobData(TypedDict):
7979
"network-advanced": {
8080
"description": "a group of network tests with complex prerequisites",
8181
"requirements": [
82-
"A pool with at least 1 host.",
82+
"A pool with at least 1 host (if more, with same network configuration).",
8383
"At least 2 free NICs on every host.",
8484
"A small VM that can be imported on the SRs.",
8585
],

tests/network/conftest.py

Lines changed: 196 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22

33
import pytest
44

5+
import json
56
import logging
7+
import re
68

79
from data import HOST_FREE_NICS
810
from lib.common import PackageManagerEnum
911
from lib.host import Host
1012
from lib.network import Network
13+
from lib.tunnel import Tunnel
14+
from lib.typing import JSONType
15+
from lib.vlan import VLAN
1116
from lib.vm import VM
17+
from lib.xo import xo_cli
1218

1319
from typing import Generator
1420

@@ -18,6 +24,102 @@ def host_no_sdn_controller(host: Host) -> None:
1824
if host.xe('sdn-controller-list', minimal=True):
1925
pytest.fail("This test requires an XCP-ng with no SDN controller")
2026

27+
RE_digits = re.compile(r'(\d+)')
28+
29+
def compare_versions(a: str, b: str) -> int:
30+
"""
31+
Return 1 if a > b, -1 if a < b, 0 if equal.
32+
Extracts successive digit groups from each string and compares them as integers.
33+
Non-digit separators are ignored. Missing groups are treated as 0.
34+
"""
35+
match_a = RE_digits.search(a)
36+
match_b = RE_digits.search(b)
37+
38+
while match_a or match_b:
39+
num_a = int(match_a.group(1)) if match_a else 0
40+
num_b = int(match_b.group(1)) if match_b else 0
41+
42+
if num_a != num_b:
43+
return 1 if num_a > num_b else -1
44+
45+
# advance positions past the matched groups
46+
pos_a = (match_a.end() if match_a else len(a))
47+
pos_b = (match_b.end() if match_b else len(b))
48+
49+
match_a = RE_digits.search(a, pos_a)
50+
match_b = RE_digits.search(b, pos_b)
51+
52+
return 0
53+
54+
55+
@pytest.fixture(scope='package')
56+
@pytest.mark.usefixtures('host_at_least_8_3')
57+
def hosts_with_traffic_rules(hosts_with_xo: list[Host]) -> Generator[list[Host], None, None]:
58+
"""A list of XCP-ng hosts with proper traffic rules configuration."""
59+
hosts = hosts_with_xo
60+
61+
# check XO: check sdn-controller plugin (loaded + minimal version)
62+
minimal = "1.3.0"
63+
64+
plugin_found = False
65+
plugins = xo_cli('plugin.get', use_json=True)
66+
assert isinstance(plugins, list)
67+
for plugin in plugins:
68+
assert isinstance(plugin, dict)
69+
if plugin.get('id') != 'sdn-controller':
70+
continue
71+
72+
plugin_found = True
73+
loaded = plugin.get('loaded', False)
74+
assert isinstance(loaded, bool)
75+
if loaded:
76+
version = plugin.get('version', '')
77+
assert isinstance(version, str)
78+
if compare_versions(minimal, version) > 0:
79+
pytest.fail(f"This test requires XO with at least sdn-controller version {minimal}")
80+
else:
81+
pytest.fail("This test requires XO with sdn-controller plugin loaded")
82+
83+
if not plugin_found:
84+
pytest.fail("This test requires XO with sdn-controller plugin")
85+
86+
# check host: xcp-ng-xapi-plugins minimal version
87+
minimal = "1.16.0"
88+
89+
def host_with_xcp_ng_xapi_plugins(host: Host):
90+
# get the package version
91+
packages = json.loads(host.xe('host-call-plugin', {
92+
'host-uuid': host.uuid,
93+
'plugin': 'updater.py',
94+
'fn': 'query_installed',
95+
'args:packages': 'xcp-ng-xapi-plugins',
96+
}, minimal=True))
97+
98+
return compare_versions(minimal, packages.get('xcp-ng-xapi-plugins', '')) <= 0
99+
100+
hosts = list(filter(host_with_xcp_ng_xapi_plugins, hosts))
101+
if len(hosts) == 0:
102+
pytest.fail(f"This test requires hosts with at least xcp-ng-xapi-plugins version {minimal}")
103+
104+
# check XO: check sdn-controller configuration: should be using xapi-plugin method for OpenFlow rules
105+
def host_with_xapiplugin(host: Host) -> bool:
106+
# the key 'xo:sdn-controller:of-method' is present since cycle XO 6.5c 2026-05-14 (xo-lite v0.21.0)
107+
of_method = host.pool.param_get(
108+
'other-config',
109+
key='xo:sdn-controller:of-method',
110+
accept_unknown_key=True,
111+
) or 'channel'
112+
113+
return of_method == 'xapi-plugin'
114+
115+
hosts = list(filter(host_with_xapiplugin, hosts))
116+
if len(hosts) == 0:
117+
pytest.fail("This test requires XO to use of-method=xapi-plugin "
118+
"(see https://docs.xen-orchestra.com/xo5/configuration#sdn-controller-mode)")
119+
120+
yield hosts
121+
122+
21123
# a clone of imported_vm in which we've added tcpdump
22124
# not to be used by tests directly
23125
@pytest.fixture(scope='module')
@@ -49,12 +151,8 @@ def vm_with_tcpdump_scope_function(vm_with_tcpdump_scope_module: VM):
49151
yield vm
50152
vm.destroy()
51153

52-
@pytest.fixture(scope='module')
53-
def empty_network(host: Host) -> Generator[Network, None, None]:
54-
net = host.create_network(label="empty_network for tests")
55-
yield net
56-
net.destroy()
57154

155+
# ---- Bond ----
58156
@pytest.fixture(scope='function')
59157
def bond_lacp(host: Host, empty_network: Network):
60158
if len(HOST_FREE_NICS) < 2:
@@ -102,3 +200,96 @@ def bond_balanceslb(host: Host, empty_network: Network):
102200
bond = host.create_bond(empty_network, pifs, mode="balance-slb")
103201
yield bond
104202
bond.destroy()
203+
204+
205+
# ---- Network ----
206+
@pytest.fixture(scope='module')
207+
def empty_network(host: Host) -> Generator[Network, None, None]:
208+
net = host.create_network(label="empty_network for tests")
209+
yield net
210+
net.destroy()
211+
212+
213+
# ---- Tunnel ----
214+
@pytest.fixture(params=["gre", "vxlan"])
215+
def tunnel_protocol(request: pytest.FixtureRequest) -> str:
216+
return request.param
217+
218+
@pytest.fixture(params=[False, True])
219+
def tunnel_encryption(request: pytest.FixtureRequest) -> bool:
220+
return request.param
221+
222+
@pytest.fixture
223+
def tunnel(
224+
hosts_with_xo: list[Host],
225+
tunnel_protocol: str, tunnel_encryption: bool,
226+
) -> Generator[Tunnel, None, None]:
227+
host = hosts_with_xo[0]
228+
229+
# check system requirements
230+
if not host.is_package_installed("openvswitch-ipsec"):
231+
pytest.fail("'tunnel' fixture requires configuration, see https://docs.xen-orchestra.com/sdn_controller")
232+
233+
# create a tunnel over the management PIF
234+
tunnel_device = host.management_pif().device()
235+
236+
logging.info(f"tunnel: resolve PIF on {host.hostname_or_ip} using \
237+
{[(pif.network_uuid(), pif.device()) for pif in host.pifs()]}")
238+
239+
[pif] = host.pifs(device=tunnel_device)
240+
if pif.ip_configuration_mode() == "None":
241+
pytest.fail(f"'tunnel' fixture requires tunnel_device={tunnel_device} to have configured IP")
242+
243+
existing_tunnels = [t.uuid for t in host.tunnels()]
244+
logging.info(f"tunnel: existing tunnels: {existing_tunnels}")
245+
246+
xo_cli('sdnController.createPrivateNetwork', {
247+
'poolIds': f"json:[\"{host.pool.uuid}\"]",
248+
'pifIds': f"json:[\"{pif.uuid}\"]",
249+
'name': 'test-tunnel',
250+
'description': 'tunnel for test',
251+
'encapsulation': tunnel_protocol,
252+
'encrypted': 'true' if tunnel_encryption else 'false',
253+
})
254+
255+
# sdnController.createPrivateNetwork might have created several Tunnel (one per host)
256+
# so get all created Tunnel
257+
created_tunnels = list(set([t.uuid for t in host.tunnels()]) - set(existing_tunnels))
258+
logging.info(f"tunnel: created tunnels: {created_tunnels}")
259+
260+
# yield only the first tunnel
261+
yield Tunnel(host, created_tunnels[0])
262+
263+
# teardown created_tunnels (and associated networks)
264+
network_uuids: set[str] = set()
265+
266+
for tunnel_uuid in created_tunnels:
267+
tunnel = Tunnel(host, tunnel_uuid)
268+
269+
# get network linked to the tunnel
270+
network_uuids.add(tunnel.access_PIF().network_uuid())
271+
272+
# destroy the tunnel
273+
tunnel.destroy()
274+
275+
# destroy networks associated to destroyed tunnels
276+
for network_uuid in network_uuids:
277+
Network(host, network_uuid).destroy()
278+
279+
280+
# ---- VLAN ----
281+
@pytest.fixture
282+
def vlan(host: Host, empty_network: Network) -> Generator[VLAN, None, None]:
283+
logging.info(f"vlan: resolve PIF on {host.hostname_or_ip} using \
284+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
285+
286+
if len(HOST_FREE_NICS) < 1:
287+
pytest.fail("This fixture needs at least 1 free NICs")
288+
289+
# randomly chosen tag
290+
vlan_tag = 42
291+
292+
[pif] = host.pifs(device=HOST_FREE_NICS[0])
293+
vlan = host.create_vlan(empty_network, pif, vlan_tag)
294+
yield vlan
295+
vlan.destroy()

0 commit comments

Comments
 (0)