Skip to content

Commit ea14d95

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 011041e commit ea14d95

3 files changed

Lines changed: 1011 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: 199 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,101 @@ 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+
def hosts_with_traffic_rules(hosts_with_xo: list[Host]) -> Generator[list[Host], None, None]:
57+
"""A list of XCP-ng hosts with proper traffic rules configuration."""
58+
hosts = hosts_with_xo
59+
60+
# check XO: check sdn-controller plugin (loaded + minimal version)
61+
minimal = "1.3.0"
62+
63+
plugin_found = False
64+
plugins = xo_cli('plugin.get', use_json=True)
65+
assert isinstance(plugins, list)
66+
for plugin in plugins:
67+
assert isinstance(plugin, dict)
68+
if plugin.get('id') != 'sdn-controller':
69+
continue
70+
71+
plugin_found = True
72+
loaded = plugin.get('loaded', False)
73+
assert isinstance(loaded, bool)
74+
if loaded:
75+
version = plugin.get('version', '')
76+
assert isinstance(version, str)
77+
if compare_versions(minimal, version) > 0:
78+
pytest.fail(f"This test requires XO with at least sdn-controller version {minimal}")
79+
else:
80+
pytest.fail("This test requires XO with sdn-controller plugin loaded")
81+
82+
if not plugin_found:
83+
pytest.fail("This test requires XO with sdn-controller plugin")
84+
85+
# check host: xcp-ng-xapi-plugins minimal version
86+
minimal = "1.16.0"
87+
88+
def host_with_xcp_ng_xapi_plugins(host: Host):
89+
# get the package version
90+
packages = json.loads(host.xe('host-call-plugin', {
91+
'host-uuid': host.uuid,
92+
'plugin': 'updater.py',
93+
'fn': 'query_installed',
94+
'args:packages': 'xcp-ng-xapi-plugins',
95+
}, minimal=True))
96+
97+
return compare_versions(minimal, packages.get('xcp-ng-xapi-plugins', '')) <= 0
98+
99+
hosts = list(filter(host_with_xcp_ng_xapi_plugins, hosts))
100+
if len(hosts) == 0:
101+
pytest.fail(f"This test requires hosts with at least xcp-ng-xapi-plugins version {minimal}")
102+
103+
# check XO: check sdn-controller configuration: should be using xapi-plugin method for OpenFlow rules
104+
def host_with_xapiplugin(host: Host) -> bool:
105+
# the key 'xo:sdn-controller:of-method' is present since cycle XO 6.5c 2026-05-14 (xo-lite v0.21.0)
106+
of_method = host.pool.param_get(
107+
'other-config',
108+
key='xo:sdn-controller:of-method',
109+
accept_unknown_key=True,
110+
) or 'channel'
111+
112+
return of_method == 'xapi-plugin'
113+
114+
hosts = list(filter(host_with_xapiplugin, hosts))
115+
if len(hosts) == 0:
116+
pytest.fail("This test requires XO to use of-method=xapi-plugin "
117+
"(see https://docs.xen-orchestra.com/xo5/configuration#sdn-controller-mode)")
118+
119+
yield hosts
120+
121+
21122
# a clone of imported_vm in which we've added tcpdump
22123
# not to be used by tests directly
23124
@pytest.fixture(scope='module')
@@ -50,12 +151,8 @@ def vm_with_tcpdump_scope_function(vm_with_tcpdump_scope_module: VM):
50151
yield vm
51152
vm.destroy()
52153

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

155+
# ---- Bond ----
59156
@pytest.fixture(scope='function')
60157
def bond_lacp(host: Host, empty_network: Network):
61158
if len(HOST_FREE_NICS) < 2:
@@ -103,3 +200,100 @@ def bond_balanceslb(host: Host, empty_network: Network):
103200
bond = host.create_bond(empty_network, pifs, mode="balance-slb")
104201
yield bond
105202
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+
# we could have several pifs on one device (due to VLANs for example)
240+
pifs = [pif for pif in host.pifs(device=tunnel_device) if pif.ip_configuration_mode() != "None"]
241+
if len(pifs) == 0:
242+
pytest.fail(f"'tunnel' fixture requires tunnel_device={tunnel_device} to have configured IP")
243+
244+
# use the first usable pif
245+
pif = pifs[0]
246+
247+
existing_tunnels = [t.uuid for t in host.tunnels()]
248+
logging.info(f"tunnel: existing tunnels: {existing_tunnels}")
249+
250+
xo_cli('sdnController.createPrivateNetwork', {
251+
'poolIds': f"json:[\"{host.pool.uuid}\"]",
252+
'pifIds': f"json:[\"{pif.uuid}\"]",
253+
'name': 'test-tunnel',
254+
'description': 'tunnel for test',
255+
'encapsulation': tunnel_protocol,
256+
'encrypted': 'true' if tunnel_encryption else 'false',
257+
})
258+
259+
# sdnController.createPrivateNetwork might have created several Tunnel (one per host)
260+
# so get all created Tunnel
261+
created_tunnels = list(set([t.uuid for t in host.tunnels()]) - set(existing_tunnels))
262+
logging.info(f"tunnel: created tunnels: {created_tunnels}")
263+
264+
# yield only the first tunnel
265+
yield Tunnel(host, created_tunnels[0])
266+
267+
# teardown created_tunnels (and associated networks)
268+
network_uuids: set[str] = set()
269+
270+
for tunnel_uuid in created_tunnels:
271+
tunnel = Tunnel(host, tunnel_uuid)
272+
273+
# get network linked to the tunnel
274+
network_uuids.add(tunnel.access_PIF().network_uuid())
275+
276+
# destroy the tunnel
277+
tunnel.destroy()
278+
279+
# destroy networks associated to destroyed tunnels
280+
for network_uuid in network_uuids:
281+
Network(host, network_uuid).destroy()
282+
283+
284+
# ---- VLAN ----
285+
@pytest.fixture
286+
def vlan(host: Host, empty_network: Network) -> Generator[VLAN, None, None]:
287+
logging.info(f"vlan: resolve PIF on {host.hostname_or_ip} using \
288+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
289+
290+
if len(HOST_FREE_NICS) < 1:
291+
pytest.fail("This fixture needs at least 1 free NICs")
292+
293+
# randomly chosen tag
294+
vlan_tag = 42
295+
296+
[pif] = host.pifs(device=HOST_FREE_NICS[0])
297+
vlan = host.create_vlan(empty_network, pif, vlan_tag)
298+
yield vlan
299+
vlan.destroy()

0 commit comments

Comments
 (0)