Skip to content

Commit 44f0b9d

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 4d996d0 commit 44f0b9d

3 files changed

Lines changed: 1035 additions & 14 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: 222 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,123 @@
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

13-
from typing import Generator
19+
from typing import Generator, Literal
1420

1521
@pytest.fixture(scope='package')
1622
def host_no_sdn_controller(host: Host) -> None:
1723
""" An XCP-ng with no SDN controller. """
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.17.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,19 +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='function')
54-
def empty_network(host: Host) -> Generator[Network, None, None]:
55-
net = host.create_network(label="empty_network for tests")
56-
57-
yield net
58-
59-
for vif_uuid in net.vif_uuids():
60-
host.xe("vif-unplug", {
61-
'uuid': vif_uuid,
62-
})
63-
64-
net.destroy()
65154

155+
# ---- Bond ----
66156
@pytest.fixture(scope='function')
67157
def bond_lacp(host: Host, empty_network: Network):
68158
if len(HOST_FREE_NICS) < 2:
@@ -110,3 +200,122 @@ def bond_balanceslb(host: Host, empty_network: Network):
110200
bond = host.create_bond(empty_network, pifs, mode="balance-slb")
111201
yield bond
112202
bond.destroy()
203+
204+
205+
# ---- Network ----
206+
@pytest.fixture(scope='function')
207+
def empty_network(host: Host) -> Generator[Network, None, None]:
208+
net = host.create_network(label="empty_network for tests")
209+
210+
yield net
211+
212+
for vif_uuid in net.vif_uuids():
213+
host.xe("vif-unplug", {
214+
'uuid': vif_uuid,
215+
})
216+
217+
net.destroy()
218+
219+
220+
# ---- Tunnel ----
221+
@pytest.fixture(params=["gre", "vxlan"])
222+
def tunnel_protocol(request: pytest.FixtureRequest) -> str:
223+
return request.param
224+
225+
@pytest.fixture(params=[False, True])
226+
def tunnel_encryption(request: pytest.FixtureRequest) -> bool:
227+
return request.param
228+
229+
@pytest.fixture
230+
def tunnel(
231+
hosts_with_xo: list[Host],
232+
tunnel_protocol: str, tunnel_encryption: bool,
233+
) -> Generator[Tunnel, None, None]:
234+
host = hosts_with_xo[0]
235+
236+
# check system requirements
237+
prepare: dict[str, Literal[True]] = {}
238+
if not host.is_package_installed("openvswitch-ipsec"):
239+
prepare["installed-openvswitch-ipsec"] = True
240+
host.yum_install(["openvswitch-ipsec"])
241+
if not host.service_started("ipsec"):
242+
prepare["service-ipsec"] = True
243+
host.ssh("systemctl start ipsec")
244+
if not host.service_started("openvswitch-ipsec"):
245+
prepare["service-openvswitch-ipsec"] = True
246+
host.ssh("systemctl start openvswitch-ipsec")
247+
248+
# create a tunnel over the management PIF
249+
tunnel_device = host.management_pif().device()
250+
251+
logging.info(f"tunnel: resolve PIF on {host.hostname_or_ip} using \
252+
{[(pif.network_uuid(), pif.device()) for pif in host.pifs()]}")
253+
254+
# we could have several pifs on one device (due to VLANs for example)
255+
pifs = [pif for pif in host.pifs(device=tunnel_device) if pif.ip_configuration_mode() != "None"]
256+
if len(pifs) == 0:
257+
pytest.fail(f"'tunnel' fixture requires tunnel_device={tunnel_device} to have configured IP")
258+
259+
# use the first usable pif
260+
pif = pifs[0]
261+
262+
existing_tunnels = [t.uuid for t in host.tunnels()]
263+
logging.info(f"tunnel: existing tunnels: {existing_tunnels}")
264+
265+
xo_cli('sdnController.createPrivateNetwork', {
266+
'poolIds': f"json:[\"{host.pool.uuid}\"]",
267+
'pifIds': f"json:[\"{pif.uuid}\"]",
268+
'name': 'test-tunnel',
269+
'description': 'tunnel for test',
270+
'encapsulation': tunnel_protocol,
271+
'encrypted': 'true' if tunnel_encryption else 'false',
272+
})
273+
274+
# sdnController.createPrivateNetwork might have created several Tunnel (one per host)
275+
# so get all created Tunnel
276+
created_tunnels = list(set([t.uuid for t in host.tunnels()]) - set(existing_tunnels))
277+
logging.info(f"tunnel: created tunnels: {created_tunnels}")
278+
279+
# yield only the first tunnel
280+
yield Tunnel(host, created_tunnels[0])
281+
282+
# teardown created_tunnels (and associated networks)
283+
network_uuids: set[str] = set()
284+
285+
for tunnel_uuid in created_tunnels:
286+
tunnel = Tunnel(host, tunnel_uuid)
287+
288+
# get network linked to the tunnel
289+
network_uuids.add(tunnel.access_pif().network_uuid())
290+
291+
# destroy the tunnel
292+
tunnel.destroy()
293+
294+
# destroy networks associated to destroyed tunnels
295+
for network_uuid in network_uuids:
296+
Network(host, network_uuid).destroy()
297+
298+
# remove installed dependencies
299+
if "service-openvswitch-ipsec" in prepare:
300+
host.ssh("systemctl stop openvswitch-ipsec")
301+
if "service-ipsec" in prepare:
302+
host.ssh("systemctl stop ipsec")
303+
if "installed-openvswitch-ipsec" in prepare:
304+
host.yum_remove(["openvswitch-ipsec"])
305+
306+
# ---- VLAN ----
307+
@pytest.fixture
308+
def vlan(host: Host, empty_network: Network) -> Generator[VLAN, None, None]:
309+
logging.info(f"vlan: resolve PIF on {host.hostname_or_ip} using \
310+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
311+
312+
if len(HOST_FREE_NICS) < 1:
313+
pytest.fail("This fixture needs at least 1 free NICs")
314+
315+
# randomly chosen tag
316+
vlan_tag = 42
317+
318+
[pif] = host.pifs(device=HOST_FREE_NICS[0])
319+
vlan = host.create_vlan(empty_network, pif, vlan_tag)
320+
yield vlan
321+
vlan.destroy()

0 commit comments

Comments
 (0)