Skip to content

Commit cfc45cc

Browse files
committed
Add few bond tests
- add some tests for Bond - add new abstractions (Network and Bond) with fixtures Signed-off-by: Sebastien Marie <semarie@kapouay.eu.org>
1 parent 76c146f commit cfc45cc

6 files changed

Lines changed: 292 additions & 0 deletions

File tree

data.py-dist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,7 @@ IMAGE_EQUIVS: dict[str, str] = {
269269

270270
# This should be a working DNS server that's not used by any VM images.
271271
TEST_DNS_SERVER = "1.1.1.1"
272+
273+
# List of NICs available on host for network tests.
274+
# example: HOST_FREE_NICS: list[str] = ['eth1', 'eth2']
275+
HOST_FREE_NICS: list[str] = []

jobs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ class JobData(TypedDict):
7575
"paths": ["tests/misc", "tests/migration"],
7676
"markers": "multi_vms and not flaky and not reboot",
7777
},
78+
"network-advanced": {
79+
"description": "a group of network tests with complex prerequisites",
80+
"requirements": [
81+
"A pool with at least 1 host.",
82+
"At least 2 free NICs on every host.",
83+
"A small VM that can be imported on the SRs.",
84+
],
85+
"nb_pools": 1,
86+
"params": {
87+
"--vm": "single/small_vm",
88+
},
89+
"paths": ["tests/network"],
90+
"markers": "complex_prerequisites",
91+
},
7892
"packages": {
7993
"description": "tests that packages can be installed correctly",
8094
"requirements": [

lib/pif.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ def is_currently_attached(self) -> bool:
5656
def is_management(self) -> bool:
5757
return strtobool(self.param_get("management"))
5858

59+
def device(self) -> str:
60+
device = self.param_get("device")
61+
assert device is not None
62+
return device
63+
5964
def network_uuid(self) -> str:
6065
uuid = self.param_get("network-uuid")
6166
assert uuid is not None, "unexpected PIF without network-uuid"

tests/network/conftest.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,104 @@
1+
from __future__ import annotations
2+
13
import pytest
24

5+
import logging
6+
7+
from data import HOST_FREE_NICS
8+
from lib.common import PackageManagerEnum
39
from lib.host import Host
10+
from lib.network import Network
11+
from lib.vm import VM
12+
13+
from typing import Generator
414

515
@pytest.fixture(scope='package')
616
def host_no_sdn_controller(host: Host) -> None:
717
""" An XCP-ng with no SDN controller. """
818
if host.xe('sdn-controller-list', minimal=True):
919
pytest.fail("This test requires an XCP-ng with no SDN controller")
20+
21+
# a clone of imported_vm in which we've added tcpdump
22+
# not to be used by tests directly
23+
@pytest.fixture(scope='module')
24+
def vm_with_tcpdump_scope_module(imported_vm: VM):
25+
logging.info("Preparing VM with tcpdump installed")
26+
vm = imported_vm.clone(name=f"{imported_vm.name()} with tcpdump")
27+
vm.start()
28+
vm.wait_for_vm_running_and_ssh_up()
29+
30+
# install tcpdump
31+
pkg_mgr = vm.detect_package_manager()
32+
if pkg_mgr == PackageManagerEnum.APK:
33+
vm.ssh("apk add tcpdump")
34+
elif pkg_mgr == PackageManagerEnum.APT_GET:
35+
vm.ssh("apt-get install tcpdump")
36+
elif pkg_mgr == PackageManagerEnum.RPM:
37+
# XXX assume yum for now
38+
vm.ssh("yum install tcpdump")
39+
else:
40+
pytest.fail("Package manager '%s' not supported" % pkg_mgr)
41+
42+
vm.shutdown(verify=True)
43+
yield vm
44+
vm.destroy()
45+
46+
@pytest.fixture(scope='function')
47+
def vm_with_tcpdump_scope_function(vm_with_tcpdump_scope_module: VM):
48+
vm = vm_with_tcpdump_scope_module.clone(name=f"{vm_with_tcpdump_scope_module.name()} for tests")
49+
yield vm
50+
vm.destroy()
51+
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()
57+
58+
@pytest.fixture(scope='function')
59+
def bond_lacp(host: Host, empty_network: Network):
60+
if len(HOST_FREE_NICS) < 2:
61+
pytest.fail("This fixture needs at least 2 free NICs")
62+
63+
pifs = []
64+
logging.info(f"bond: resolve PIFs on {host.hostname_or_ip} using \
65+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
66+
for name in HOST_FREE_NICS[0:2]:
67+
[pif] = host.pifs(device=name)
68+
pifs.append(pif)
69+
70+
bond = host.create_bond(empty_network, pifs, mode="lacp")
71+
yield bond
72+
bond.destroy()
73+
74+
@pytest.fixture(scope='function')
75+
def bond_activebackup(host: Host, empty_network: Network):
76+
if len(HOST_FREE_NICS) < 2:
77+
pytest.fail("This fixture needs at least 2 free NICs")
78+
79+
pifs = []
80+
logging.info(f"bond: resolve PIFs on {host.hostname_or_ip} using \
81+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
82+
for name in HOST_FREE_NICS[0:2]:
83+
[pif] = host.pifs(device=name)
84+
pifs.append(pif)
85+
86+
bond = host.create_bond(empty_network, pifs, mode="active-backup")
87+
yield bond
88+
bond.destroy()
89+
90+
@pytest.fixture(scope='function')
91+
def bond_balanceslb(host: Host, empty_network: Network):
92+
if len(HOST_FREE_NICS) < 2:
93+
pytest.fail("This fixture needs at least 2 free NICs")
94+
95+
pifs = []
96+
logging.info(f"bond: resolve PIFs on {host.hostname_or_ip} using \
97+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
98+
for name in HOST_FREE_NICS[0:2]:
99+
[pif] = host.pifs(device=name)
100+
pifs.append(pif)
101+
102+
bond = host.create_bond(empty_network, pifs, mode="balance-slb")
103+
yield bond
104+
bond.destroy()

tests/network/test_bond.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
import logging
6+
7+
from lib.bond import Bond
8+
from lib.host import Host
9+
from lib.pif import PIF
10+
from lib.vm import VM
11+
12+
# Requirements:
13+
# From --hosts parameter:
14+
# - host(A1): an XCP-ng host, with at least 2 free NICs
15+
# From --vm parameter
16+
# - A VM to import
17+
18+
def _wait_for_packet(host: Host | VM, interface: str | list[str], sfilter: str, timeout: int = 30) -> None:
19+
if isinstance(interface, list):
20+
interface = ' '.join([f"-i {iface}" for iface in interface])
21+
else:
22+
interface = f"-i {interface}"
23+
24+
host.ssh(f"timeout {timeout} tcpdump {interface} -n -c1 '{sfilter}'")
25+
26+
@pytest.mark.complex_prerequisites
27+
@pytest.mark.small_vm
28+
class TestBond:
29+
def test_lacp(self, host: Host, vm_with_tcpdump_scope_function: VM, bond_lacp: Bond):
30+
# expect host with eth1 and eth2 NICs free for use
31+
logging.info(f"Bond = {bond_lacp.uuid} mode={bond_lacp.mode()} \
32+
slaves={bond_lacp.slaves()}")
33+
34+
# disable LACP fallback (make Bond to require LACP negociation first)
35+
bond_lacp.param_set("properties", key="lacp-fallback-ab", value="false")
36+
37+
# check bond0 on the host
38+
output = host.ssh("ovs-appctl bond/show bond0")
39+
40+
if "bond_mode: balance-tcp" not in output:
41+
pytest.fail(f"string 'bond_mode: balance-tcp' not found in output. output={output}")
42+
elif "lacp_fallback_ab: false" not in output:
43+
pytest.fail(f"unexpected lacp_fallback_ab: output={output}")
44+
elif "lacp_status: configured" not in output and "lacp_status: negotiated" not in output:
45+
pytest.fail(f"unexpected lacp_status: output={output}")
46+
47+
# on the VM, add a new NIC using the bond
48+
vm = vm_with_tcpdump_scope_function
49+
vm.create_vif(1, network_uuid=bond_lacp.master().network_uuid())
50+
vm.start()
51+
vm.wait_for_vm_running_and_ssh_up()
52+
53+
vm.ssh("ip link set eth1 up")
54+
55+
# we are checking if we are seeing LACP packet on *VM side*.
56+
#
57+
# OpenvSwitch will send LACP negociation on host.eth1 and host.eth2
58+
# to establish LACP link. As we don't have other side, it will keep
59+
# sending them.
60+
# On VM side, we could see such packets. So we are checking the VM.eth1
61+
# is properly connected to the Bond just created (but we don't check that
62+
# OpenvSwitch is properly setup)
63+
logging.info("Waiting for LACP packet")
64+
_wait_for_packet(vm, "eth1", "ether proto 0x8809")
65+
66+
def test_active_backup(self, host: Host, vm_with_tcpdump_scope_function: VM, bond_activebackup: Bond):
67+
# expect host with eth1 and eth2 NICs free for use
68+
logging.info(f"Bond = {bond_activebackup.uuid} mode={bond_activebackup.mode()} \
69+
slaves={bond_activebackup.slaves()}")
70+
71+
# get device's PIFs used by the bond on the host
72+
bond_devices = [PIF(uuid, host).device() for uuid in bond_activebackup.slaves()]
73+
74+
# check bond0 on the host
75+
output = host.ssh("ovs-appctl bond/show bond0")
76+
77+
if "bond_mode: active-backup" not in output:
78+
pytest.fail(f"string 'bond_mode: active-backup' not found in output. output={output}")
79+
80+
if "lacp_status: off" not in output:
81+
pytest.fail(f"unexpected lacp_status: output={output}")
82+
83+
# on the VM, add a new NIC using the bond
84+
vm = vm_with_tcpdump_scope_function
85+
vm.create_vif(1, network_uuid=bond_activebackup.master().network_uuid())
86+
vm.start()
87+
vm.wait_for_vm_running_and_ssh_up()
88+
89+
vm.ssh("ip link set eth1 up")
90+
91+
# just check if we see a packet on the *host side*.
92+
# the VM kernel is expected to send IPv6 packet for Router Solicitation
93+
# as the VM interface is UP.
94+
logging.info("Waiting for some IPv6 packet")
95+
_wait_for_packet(host, bond_devices, "ether proto 0x86dd")
96+
97+
def test_balance_slb(self, host: Host, vm_with_tcpdump_scope_function: VM, bond_balanceslb: Bond):
98+
# expect host with eth1 and eth2 NICs free for use
99+
logging.info(f"Bond = {bond_balanceslb.uuid} mode={bond_balanceslb.mode()} \
100+
slaves={bond_balanceslb.slaves()}")
101+
102+
# get device's PIFs used by the bond on the host
103+
bond_devices = [PIF(uuid, host).device() for uuid in bond_balanceslb.slaves()]
104+
105+
# check bond0 on the host
106+
output = host.ssh("ovs-appctl bond/show bond0")
107+
108+
if "bond_mode: balance-slb" not in output:
109+
pytest.fail(f"string 'bond_mode: balance-tcp' not found in output. output={output}")
110+
111+
if "lacp_status: off" not in output:
112+
pytest.fail(f"unexpected lacp_status: output={output}")
113+
114+
# on the VM, add a new NIC using the bond
115+
vm = vm_with_tcpdump_scope_function
116+
vm.create_vif(1, network_uuid=bond_balanceslb.master().network_uuid())
117+
vm.start()
118+
vm.wait_for_vm_running_and_ssh_up()
119+
120+
vm.ssh("ip link set eth1 up")
121+
122+
# just check if we see a packet on the *host side*.
123+
# the VM kernel is expected to send IPv6 packet for Router Solicitation
124+
# as the VM interface is UP.
125+
logging.info("Waiting for some IPv6 packet")
126+
_wait_for_packet(host, bond_devices, "ether proto 0x86dd")

tests/network/test_network.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
import logging
6+
7+
from lib.common import Defer
8+
from lib.host import Host
9+
from lib.network import Network
10+
from lib.vm import VM
11+
12+
class TestNetwork:
13+
@pytest.mark.no_vm
14+
def test_empty_network(self, host: Host, empty_network: Network):
15+
assert empty_network.pif_uuids() == [], "PIF list must be empty"
16+
assert empty_network.vif_uuids() == [], "VIF list must be empty"
17+
assert empty_network.is_private(), "empty_network must be private"
18+
assert empty_network.MTU() == 1500, "expected MTU is 1500"
19+
20+
@pytest.mark.small_vm
21+
def test_private_network(self, host: Host, empty_network: Network, imported_vm: VM, defer: Defer):
22+
network = empty_network
23+
24+
vm1 = imported_vm.clone()
25+
defer(lambda: vm1.destroy())
26+
vm2 = imported_vm.clone()
27+
defer(lambda: vm2.destroy())
28+
29+
vif_1_1 = vm1.create_vif(1, network_uuid=network.uuid)
30+
vif_2_1 = vm2.create_vif(1, network_uuid=network.uuid)
31+
32+
assert len(vm1.vifs()) == 2, "VM1 should have 2 NICs"
33+
assert len(vm2.vifs()) == 2, "VM2 should have 2 NICs"
34+
assert len(network.vif_uuids()) == 2, "network have 2 VIFs"
35+
36+
vm1.start()
37+
vm2.start()
38+
39+
vm1.wait_for_vm_running_and_ssh_up()
40+
vm2.wait_for_vm_running_and_ssh_up()
41+
42+
logging.info("Configuring local address on private network")
43+
vm1.ssh(f"ifconfig eth{vif_1_1.param_get('device')} inet 169.254.1.1 broadcast 169.254.0.0 up")
44+
vm2.ssh(f"ifconfig eth{vif_2_1.param_get('device')} inet 169.254.2.1 broadcast 169.254.0.0 up")
45+
46+
logging.info("Ping VMs")
47+
vm1.ssh("ping -c3 -w5 169.254.2.1")
48+
vm2.ssh("ping -c3 -w5 169.254.1.1")

0 commit comments

Comments
 (0)