Skip to content

Commit 8a13f54

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 on each tests: - a traffic rule to block some traffic is added and after that removed - VM lifecycle is tested with the rule applied - each step is verified (is the traffic blocked as expected ?) - the initial state is verified (to ensure no currently active traffic rule will pertubate the test) - the final state is verified (to ensure no rules are left) the traffic check is done using low-level debug cli tool from OVS. It permits to test the openflow rule application without sending any packet. a check is done for each port plugged on the virtual switch. the two first "simple" rules are the more complexes: - interleaving two rules addition/removal is tested - VM is started, stopped, restarted (rules are managed dynamically and on the fly by XO) 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 80fd925 commit 8a13f54

3 files changed

Lines changed: 1045 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: 195 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,96 @@
22

33
import pytest
44

5+
import json
56
import logging
67

8+
from rpm_version import Evr # type: ignore[import-untyped]
9+
710
from data import HOST_FREE_NICS
811
from lib.common import PackageManagerEnum
912
from lib.host import Host
1013
from lib.network import Network
14+
from lib.tunnel import Tunnel
15+
from lib.typing import JSONType
16+
from lib.vlan import VLAN
1117
from lib.vm import VM
18+
from lib.xo import xo_cli
1219

13-
from typing import Generator
20+
from typing import Generator, Literal
1421

1522
@pytest.fixture(scope='package')
1623
def host_no_sdn_controller(host: Host) -> None:
1724
""" An XCP-ng with no SDN controller. """
1825
if host.xe('sdn-controller-list', minimal=True):
1926
pytest.fail("This test requires an XCP-ng with no SDN controller")
2027

28+
@pytest.fixture(scope='package')
29+
def hosts_with_traffic_rules(hosts_with_xo: list[Host]) -> Generator[list[Host], None, None]:
30+
"""A list of XCP-ng hosts with proper traffic rules configuration."""
31+
hosts = hosts_with_xo
32+
33+
# check XO: check sdn-controller plugin (loaded + minimal version)
34+
minimal = Evr.parse("1.3.0")
35+
36+
plugin_found = False
37+
plugins = xo_cli('plugin.get', use_json=True)
38+
assert isinstance(plugins, list)
39+
for plugin in plugins:
40+
assert isinstance(plugin, dict)
41+
if plugin.get('id') != 'sdn-controller':
42+
continue
43+
44+
plugin_found = True
45+
loaded = plugin.get('loaded', False)
46+
assert isinstance(loaded, bool)
47+
if loaded:
48+
version = plugin.get('version', '')
49+
assert isinstance(version, str)
50+
if minimal > Evr.parse(version):
51+
pytest.fail(f"This test requires XO with at least sdn-controller version {minimal}")
52+
else:
53+
pytest.fail("This test requires XO with sdn-controller plugin loaded")
54+
55+
if not plugin_found:
56+
pytest.fail("This test requires XO with sdn-controller plugin")
57+
58+
# check host: xcp-ng-xapi-plugins minimal version
59+
minimal = Evr.parse("xcp-ng-xapi-plugins-1.17.0")
60+
61+
def host_with_xcp_ng_xapi_plugins(host: Host):
62+
# get the package version
63+
packages = json.loads(host.xe('host-call-plugin', {
64+
'host-uuid': host.uuid,
65+
'plugin': 'updater.py',
66+
'fn': 'query_installed',
67+
'args:packages': 'xcp-ng-xapi-plugins',
68+
}, minimal=True))
69+
70+
return minimal <= Evr.parse(packages.get('xcp-ng-xapi-plugins', ''))
71+
72+
hosts = list(filter(host_with_xcp_ng_xapi_plugins, hosts))
73+
if len(hosts) == 0:
74+
pytest.fail(f"This test requires hosts with at least xcp-ng-xapi-plugins version {minimal}")
75+
76+
# check XO: check sdn-controller configuration: should be using xapi-plugin method for OpenFlow rules
77+
def host_with_xapiplugin(host: Host) -> bool:
78+
# the key 'xo:sdn-controller:of-method' is present since cycle XO 6.5c 2026-05-14 (xo-lite v0.21.0)
79+
of_method = host.pool.param_get(
80+
'other-config',
81+
key='xo:sdn-controller:of-method',
82+
accept_unknown_key=True,
83+
) or 'channel'
84+
85+
return of_method == 'xapi-plugin'
86+
87+
hosts = list(filter(host_with_xapiplugin, hosts))
88+
if len(hosts) == 0:
89+
pytest.fail("This test requires XO to use of-method=xapi-plugin "
90+
"(see https://docs.xen-orchestra.com/xo5/configuration#sdn-controller-mode)")
91+
92+
yield hosts
93+
94+
2195
# a clone of imported_vm in which we've added tcpdump
2296
# not to be used by tests directly
2397
@pytest.fixture(scope='module')
@@ -50,19 +124,8 @@ def vm_with_tcpdump_scope_function(vm_with_tcpdump_scope_module: VM):
50124
yield vm
51125
vm.destroy()
52126

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()
65127

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

0 commit comments

Comments
 (0)