Skip to content

Commit cb4dda7

Browse files
authored
Merge pull request #390 from xcp-ng/srt/bond
Bond tests
2 parents 3f42255 + cfc45cc commit cb4dda7

11 files changed

Lines changed: 495 additions & 2 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/bond.py

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

lib/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class PackageManagerEnum(Enum):
8080
UNKNOWN = 1
8181
RPM = 2
8282
APT_GET = 3
83+
APK = 4
8384

8485
# Common VM images used in tests
8586
def vm_image(vm_key: str) -> str:

lib/host.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from packaging import version
1313

1414
import lib.commands as commands
15+
from lib.bond import Bond
1516
from lib.common import (
1617
_param_add,
1718
_param_clear,
@@ -26,6 +27,7 @@
2627
wait_for,
2728
)
2829
from lib.netutil import wrap_ip
30+
from lib.network import Network
2931
from lib.pif import PIF
3032
from lib.sr import SR
3133
from lib.vm import VM
@@ -954,3 +956,41 @@ def lvs(self, vgName: str | None = None, ignore_MGT: bool = True) -> list[str]:
954956
continue
955957
ret.append(line.strip())
956958
return ret
959+
960+
def pifs(self, device: str | None = None) -> list[PIF]:
961+
args: dict[str, str | bool | dict[str, str]] = {
962+
"host-uuid": self.uuid,
963+
}
964+
965+
if device is not None:
966+
args["device"] = device
967+
968+
return [PIF(uuid, self) for uuid in safe_split(self.xe("pif-list", args, minimal=True))]
969+
970+
def create_bond(self, network: Network, pifs: list[PIF], mode: str | None = None) -> Bond:
971+
args: dict[str, str | bool | dict[str, str]] = {
972+
'network-uuid': network.uuid,
973+
'pif-uuids': ','.join([pif.uuid for pif in pifs]),
974+
}
975+
976+
if mode is not None:
977+
args['mode'] = mode
978+
979+
uuid = self.xe("bond-create", args, minimal=True)
980+
logging.info(f"New Bond: {uuid}")
981+
982+
return Bond(self, uuid)
983+
984+
def create_network(self, label: str, description: str | None = None) -> Network:
985+
args: dict[str, str | bool | dict[str, str]] = {
986+
'name-label': label,
987+
}
988+
989+
if description is not None:
990+
args['name-description'] = description
991+
992+
logging.info(f"Creating network '{label}'")
993+
uuid = self.xe("network-create", args, minimal=True)
994+
logging.info(f"New Network: {uuid}")
995+
996+
return Network(self, uuid)

lib/network.py

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

lib/pif.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set
3+
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set, strtobool
44

55
from typing import TYPE_CHECKING, Literal, overload
66

@@ -43,3 +43,37 @@ def param_add(self, param_name: str, value: str, key: str | None = None) -> None
4343
def param_clear(self, param_name: str) -> None:
4444
_param_clear(self.host, self.xe_prefix, self.uuid,
4545
param_name)
46+
47+
def is_managed(self) -> bool:
48+
return strtobool(self.param_get("managed"))
49+
50+
def is_physical(self) -> bool:
51+
return strtobool(self.param_get("physical"))
52+
53+
def is_currently_attached(self) -> bool:
54+
return strtobool(self.param_get("currently-attached"))
55+
56+
def is_management(self) -> bool:
57+
return strtobool(self.param_get("management"))
58+
59+
def device(self) -> str:
60+
device = self.param_get("device")
61+
assert device is not None
62+
return device
63+
64+
def network_uuid(self) -> str:
65+
uuid = self.param_get("network-uuid")
66+
assert uuid is not None, "unexpected PIF without network-uuid"
67+
return uuid
68+
69+
def reconfigure_ip(self, mode: str) -> None:
70+
self.host.xe("pif-reconfigure-ip", {
71+
"uuid": self.uuid,
72+
"mode": mode,
73+
})
74+
75+
def reconfigure_ipv6(self, mode: str) -> None:
76+
self.host.xe("pif-reconfigure-ipv6", {
77+
"uuid": self.uuid,
78+
"mode": mode,
79+
})

lib/vm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ def detect_package_manager(self) -> PackageManagerEnum:
502502
return PackageManagerEnum.RPM
503503
elif self.file_exists('/usr/bin/apt-get'):
504504
return PackageManagerEnum.APT_GET
505+
elif self.file_exists('/sbin/apk'):
506+
return PackageManagerEnum.APK
505507
else:
506508
return PackageManagerEnum.UNKNOWN
507509

tests/network/conftest.py

Lines changed: 96 additions & 1 deletion
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):
9-
pytest.skip("This test requires an XCP-ng with no SDN controller")
19+
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()

0 commit comments

Comments
 (0)