Skip to content

Commit f8dd162

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 4f858ff commit f8dd162

File tree

7 files changed

+298
-2
lines changed

7 files changed

+298
-2
lines changed

jobs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,8 @@
468468
BROKEN_TESTS = [
469469
# not really broken but has complex prerequisites (3 NICs on 3 different networks)
470470
"tests/migration/test_host_evacuate.py::TestHostEvacuateWithNetwork",
471+
# not really broken but has complex prerequisites (3 NICs)
472+
"tests/network/test_bond.py::TestBond",
471473
# running quicktest on zfsvol generates dangling TAP devices that are hard to
472474
# cleanup. Bug needs to be fixed before enabling quicktest on zfsvol.
473475
"tests/storage/zfsvol/test_zfsvol_sr.py::TestZfsvolVm::test_quicktest",

lib/bond.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set, safe_split
6+
from lib.host import Host
7+
from lib.network import Network
8+
from lib.pif import PIF
9+
10+
from typing import Dict, List, Optional
11+
12+
class Bond:
13+
xe_prefix = "bond"
14+
15+
def __init__(self, host: Host, uuid: str):
16+
self.host = host
17+
self.uuid = uuid
18+
19+
def param_get(self, param_name, key=None, accept_unknown_key=False):
20+
return _param_get(self.host, Bond.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
21+
22+
def param_set(self, param_name, value, key=None):
23+
_param_set(self.host, Bond.xe_prefix, self.uuid, param_name, value, key)
24+
25+
def param_add(self, param_name, value, key=None):
26+
_param_add(self.host, Bond.xe_prefix, self.uuid, param_name, value, key)
27+
28+
def param_clear(self, param_name):
29+
_param_clear(self.host, Bond.xe_prefix, self.uuid, param_name)
30+
31+
def param_remove(self, param_name, key, accept_unknown_key=False):
32+
_param_remove(self.host, Bond.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
33+
34+
@staticmethod
35+
def create(host: Host, network: Network, pifs: List[PIF], mode: Optional[str] = None) -> Bond:
36+
args: Dict[str, str | bool] = {
37+
'network-uuid': network.uuid,
38+
'pif-uuids': ','.join([pif.uuid for pif in pifs]),
39+
}
40+
41+
if mode is not None:
42+
args['mode'] = mode
43+
44+
uuid = host.xe("bond-create", args, minimal=True)
45+
logging.info(f"New Bond: {uuid}")
46+
47+
return Bond(host, uuid)
48+
49+
def destroy(self):
50+
logging.info(f"Destroying bond: {self.uuid}")
51+
self.host.xe('bond-destroy', {'uuid': self.uuid})
52+
53+
def master(self) -> PIF:
54+
uuid = self.param_get('master')
55+
assert uuid is not None, "no master on Bond"
56+
return PIF(uuid, self.host)
57+
58+
def slaves(self) -> List[str]:
59+
return safe_split(self.param_get('slaves'))
60+
61+
def mode(self) -> Optional[str]:
62+
return self.param_get("mode")

lib/host.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,3 +767,13 @@ def lvs(self, vgName: Optional[str] = None, ignore_MGT: bool = True) -> List[str
767767
continue
768768
ret.append(line.strip())
769769
return ret
770+
771+
def PIFs(self, device: Optional[str] = None) -> List[pif.PIF]:
772+
args: Dict[str, str | bool] = {
773+
"host-uuid": self.uuid,
774+
}
775+
776+
if device is not None:
777+
args["device"] = device
778+
779+
return [pif.PIF(uuid, self) for uuid in safe_split(self.xe("pif-list", args, minimal=True))]

lib/network.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set, safe_split
6+
from lib.host import Host
7+
8+
from typing import Dict, List, Optional
9+
10+
class Network:
11+
xe_prefix = "network"
12+
13+
def __init__(self, host: Host, uuid: str):
14+
self.host = host
15+
self.uuid = uuid
16+
17+
def param_get(self, param_name, key=None, accept_unknown_key=False):
18+
return _param_get(self.host, Network.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
19+
20+
def param_set(self, param_name, value, key=None):
21+
_param_set(self.host, Network.xe_prefix, self.uuid, param_name, value, key)
22+
23+
def param_add(self, param_name, value, key=None):
24+
_param_add(self.host, Network.xe_prefix, self.uuid, param_name, value, key)
25+
26+
def param_clear(self, param_name):
27+
_param_clear(self.host, Network.xe_prefix, self.uuid, param_name)
28+
29+
def param_remove(self, param_name, key, accept_unknown_key=False):
30+
_param_remove(self.host, Network.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
31+
32+
@staticmethod
33+
def create(host: Host, label: str, description: Optional[str] = None) -> Network:
34+
args: Dict[str, str | bool] = {
35+
'name-label': label,
36+
}
37+
38+
if description is not None:
39+
args['name-description'] = description
40+
41+
logging.info(f"Creating network '{label}'")
42+
uuid = host.xe("network-create", args, minimal=True)
43+
logging.info(f"New Network: {uuid}")
44+
45+
return Network(host, uuid)
46+
47+
def destroy(self):
48+
logging.info(f"Destroying network '{self.param_get('name-label')}': {self.uuid}")
49+
self.host.xe('network-destroy', {'uuid': self.uuid})
50+
51+
def PIF_uuids(self) -> List[str]:
52+
return safe_split(self.param_get('PIF-uuids'), '; ')
53+
54+
def VIF_uuids(self) -> List[str]:
55+
return safe_split(self.param_get('VIF-uuids'), '; ')
56+
57+
def is_private(self) -> bool:
58+
return len(self.PIF_uuids()) == 0
59+
60+
def managed(self) -> bool:
61+
return self.param_get('managed') == 'true'
62+
63+
def MTU(self) -> int:
64+
return int(self.param_get('MTU') or '0')

lib/pif.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
from __future__ import annotations
2+
13
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set
24

5+
from typing import TYPE_CHECKING
6+
7+
if TYPE_CHECKING:
8+
from lib.host import Host
9+
310
class PIF:
411
xe_prefix = "pif"
512

6-
def __init__(self, uuid, host):
13+
def __init__(self, uuid: str, host: Host):
714
self.uuid = uuid
815
self.host = host
916

@@ -26,3 +33,32 @@ def param_add(self, param_name, value, key=None):
2633
def param_clear(self, param_name):
2734
_param_clear(self.host, self.xe_prefix, self.uuid,
2835
param_name)
36+
37+
def is_managed(self) -> bool:
38+
return self.param_get("managed") == "true"
39+
40+
def is_physical(self) -> bool:
41+
return self.param_get("physical") == "true"
42+
43+
def is_currently_attached(self) -> bool:
44+
return self.param_get("currently-attached") == "true"
45+
46+
def is_management(self) -> bool:
47+
return self.param_get("management") == "true"
48+
49+
def network_uuid(self) -> str:
50+
uuid = self.param_get("network-uuid")
51+
assert uuid is not None, "unexpected PIF without network-uuid"
52+
return uuid
53+
54+
def reconfigure_ip(self, mode: str) -> None:
55+
self.host.xe("pif-reconfigure-ip", {
56+
"uuid": self.uuid,
57+
"mode": mode,
58+
})
59+
60+
def reconfigure_ipv6(self, mode: str) -> None:
61+
self.host.xe("pif-reconfigure-ipv6", {
62+
"uuid": self.uuid,
63+
"mode": mode,
64+
})

tests/network/conftest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
import pytest
22

3+
from lib.host import Host
4+
from lib.network import Network
5+
6+
from typing import Generator
7+
38
@pytest.fixture(scope='package')
4-
def host_no_sdn_controller(host):
9+
def host_no_sdn_controller(host: Host):
510
""" An XCP-ng with no SDN controller. """
611
if host.xe('sdn-controller-list', minimal=True):
712
pytest.skip("This test requires an XCP-ng with no SDN controller")
13+
14+
15+
@pytest.fixture(scope='module')
16+
def empty_network(host: Host) -> Generator[Network, None, None]:
17+
try:
18+
net = Network.create(host, label="empty_network for tests")
19+
yield net
20+
finally:
21+
net.destroy()

tests/network/test_bond.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import pytest
2+
3+
import logging
4+
from contextlib import contextmanager
5+
6+
from lib.bond import Bond
7+
from lib.host import Host
8+
from lib.network import Network
9+
from lib.vm import VM
10+
11+
from typing import List
12+
13+
# Requirements:
14+
# From --hosts parameter:
15+
# - host(A1): first XCP-ng host, with at least 3 NICs (1 management, 2 unused)
16+
# From --vm parameter
17+
# - A VM to import
18+
19+
@contextmanager
20+
def managed_bond(host: Host, network: Network, devices: List[str], mode: str):
21+
pifs = []
22+
for name in devices:
23+
[pif] = host.PIFs(device=name)
24+
pifs.append(pif)
25+
26+
bond = Bond.create(host, network, pifs, mode=mode)
27+
try:
28+
yield bond
29+
finally:
30+
bond.destroy()
31+
32+
@pytest.mark.small_vm
33+
class TestNetwork:
34+
@pytest.mark.no_vm
35+
def test_basic(self, host: Host, empty_network: Network):
36+
assert len(empty_network.PIF_uuids()) == 0, "unexpected number of PIFs in network"
37+
assert len(empty_network.VIF_uuids()) == 0, "unexpected number of VIFs in network"
38+
assert empty_network.is_private(), "empty_network isn't private"
39+
assert empty_network.MTU() == 1500, "unexpected MTU"
40+
41+
def test_private_network(self, host: Host, empty_network: Network, imported_vm: VM):
42+
network = empty_network
43+
44+
try:
45+
vm1 = imported_vm.clone()
46+
vm2 = imported_vm.clone()
47+
48+
vif_1_1 = vm1.create_vif(1, network_uuid=network.uuid)
49+
vif_2_1 = vm2.create_vif(1, network_uuid=network.uuid)
50+
51+
assert len(vm1.vifs()) == 2, "VM1 should have 2 NICs"
52+
assert len(vm2.vifs()) == 2, "VM2 should have 2 NICs"
53+
assert len(network.VIF_uuids()) == 2, "unexpected number of VIFs in network"
54+
55+
vm1.start()
56+
vm2.start()
57+
58+
vm1.wait_for_vm_running_and_ssh_up()
59+
vm2.wait_for_vm_running_and_ssh_up()
60+
61+
logging.info("Configuring local address on private network")
62+
vm1.ssh(f"ifconfig eth{vif_1_1.param_get('device')} inet 169.254.1.1 broadcast 169.254.0.0 up")
63+
vm2.ssh(f"ifconfig eth{vif_2_1.param_get('device')} inet 169.254.2.1 broadcast 169.254.0.0 up")
64+
65+
logging.info("Ping VMs")
66+
assert vm1.ssh_with_result(["ping", "-c3", "-w5", "169.254.2.1"]).returncode == 0
67+
assert vm2.ssh_with_result(["ping", "-c3", "-w5", "169.254.1.1"]).returncode == 0
68+
69+
finally:
70+
# VIFs are destroyed by VM.destroy()
71+
vm2.destroy()
72+
vm1.destroy()
73+
74+
@pytest.mark.complex_prerequisites
75+
@pytest.mark.small_vm
76+
class TestBond:
77+
def test_lacp(self, host: Host, empty_network: Network, imported_vm: VM):
78+
try:
79+
vm = imported_vm.clone()
80+
network = empty_network
81+
assert len(network.PIF_uuids()) == 0
82+
83+
# expect host with eth1 and eth2 NICs free for use
84+
with managed_bond(host, network, ["eth1", "eth2"], "lacp") as bond:
85+
logging.info(f"Bond = {bond.uuid} mode={bond.mode()} slaves={bond.slaves()}")
86+
87+
# disable LACP fallback (make Bond to require LACP negociation first)
88+
bond.param_set("properties", key="lacp-fallback-ab", value=False)
89+
90+
# add new vif
91+
vm.create_vif(1, network_uuid=bond.master().network_uuid())
92+
93+
vm.start()
94+
vm.wait_for_vm_running_and_ssh_up()
95+
96+
# XXX check on host side
97+
vm.ssh('apk add tcpdump')
98+
vm.ssh('ip link set eth1 up')
99+
100+
logging.info("Waiting for LACP packet")
101+
ret = vm.ssh_with_result('timeout 30 tcpdump -i eth1 -n -c1 "ether proto 0x8809"')
102+
if ret.returncode == 124:
103+
pytest.fail("timeout, no LACP frame on eth1")
104+
elif ret.returncode != 0:
105+
pytest.fail(f"tcpdump error: code={ret.returncode} stdout={ret.stdout}")
106+
107+
finally:
108+
vm.destroy()

0 commit comments

Comments
 (0)