Skip to content

Commit 0a92846

Browse files
authored
tests, net, bgp: add connectivity test suite (RedHatQE#2121)
* move udn_vm function to libs udn_vm is a useful function and should be placed in a more common location. Signed-off-by: Sergei Volkov <sevolkov@redhat.com> * add traffic generator client for pod For some test suites, such as BGP, it is necessary to use a traffic generator inside the pod, rather than in a virtual machine. This commit adds a PodClient class that represents the tcp client running in the pod. Also this commit adds "--connect-timeout 300" option to iperf3 cmd to fix incorrect behavior and prevent false positives if there is no direct connectivity between the server and the client. Signed-off-by: Sergei Volkov <sevolkov@redhat.com> * add bgp connectivity test suite Initial BGP testing includes checking the tcp connection between the CUDN VM and the external network, as well as this connection preservation during VM migration. Signed-off-by: Sergei Volkov <sevolkov@redhat.com> * quarantine tests while CNV-69734 is open, tests should be quarantined Signed-off-by: Sergei Volkov <sevolkov@redhat.com> --------- Signed-off-by: Sergei Volkov <sevolkov@redhat.com>
1 parent 8ea4f4a commit 0a92846

8 files changed

Lines changed: 184 additions & 48 deletions

File tree

conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"node_remediation",
7979
"swap",
8080
"numa",
81+
"bgp",
8182
]
8283

8384
TEAM_MARKERS = {

libs/net/traffic_generator.py

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
2+
from abc import ABC, abstractmethod
23
from typing import Final
34

5+
from ocp_resources.pod import Pod
46
from ocp_utilities.exceptions import CommandExecFailed
5-
from timeout_sampler import TimeoutExpiredError, TimeoutSampler
7+
from timeout_sampler import TimeoutExpiredError, TimeoutSampler, retry
68

79
from libs.vm.vm import BaseVirtualMachine
810

@@ -13,6 +15,27 @@
1315
LOGGER = logging.getLogger(__name__)
1416

1517

18+
class BaseClient(ABC):
19+
"""Base abstract class for network traffic generator client."""
20+
21+
def __init__(self, server_ip: str, server_port: int):
22+
self._server_ip = server_ip
23+
self.server_port = server_port
24+
self._cmd = f"{_IPERF_BIN} --client {self._server_ip} --time 0 --port {self.server_port} --connect-timeout 300"
25+
26+
@abstractmethod
27+
def __enter__(self) -> "BaseClient":
28+
pass
29+
30+
@abstractmethod
31+
def __exit__(self, exc_type: BaseException, exc_value: BaseException, traceback: object) -> None:
32+
pass
33+
34+
@abstractmethod
35+
def is_running(self) -> bool:
36+
pass
37+
38+
1639
class Server:
1740
"""
1841
Represents a server running on a virtual machine for testing network performance.
@@ -50,7 +73,7 @@ def is_running(self) -> bool:
5073
return _is_process_running(vm=self._vm, cmd=self._cmd)
5174

5275

53-
class Client:
76+
class Client(BaseClient):
5477
"""
5578
Represents a client that connects to a server to test network performance.
5679
Implemented with iperf3
@@ -67,10 +90,8 @@ def __init__(
6790
server_ip: str,
6891
server_port: int,
6992
):
93+
super().__init__(server_ip=server_ip, server_port=server_port)
7094
self._vm = vm
71-
self._server_ip = server_ip
72-
self._server_port = server_port
73-
self._cmd = f"{_IPERF_BIN} --client {self._server_ip} --time 0 --port {self._server_port}"
7495

7596
def __enter__(self) -> "Client":
7697
self._vm.console(
@@ -115,5 +136,47 @@ def _is_process_running( # type: ignore[return]
115136
return False
116137

117138

118-
def is_tcp_connection(server: Server, client: Client) -> bool:
139+
class PodClient(BaseClient):
140+
"""Represents a TCP client that connects to a server to test network performance.
141+
142+
Expects pod to have iperf3 container.
143+
144+
Args:
145+
pod (Pod): The pod where the client runs.
146+
server_ip (str): The destination IP address of the server the client connects to.
147+
server_port (int): The port on which the server listens for connections.
148+
bind_interface (str): The interface or IP address to bind the client to (optional).
149+
If not specified, the client will use the default interface.
150+
"""
151+
152+
def __init__(self, pod: Pod, server_ip: str, server_port: int, bind_interface: str | None = None):
153+
super().__init__(server_ip=server_ip, server_port=server_port)
154+
self._pod = pod
155+
self._container = _IPERF_BIN
156+
self._cmd += f" --bind {bind_interface}" if bind_interface else ""
157+
158+
def __enter__(self) -> "PodClient":
159+
# run the command in the background using nohup to ensure it keeps running after the exec session ends
160+
self._pod.execute(
161+
command=["sh", "-c", f"nohup {self._cmd} >/tmp/{_IPERF_BIN}.log 2>&1 &"], container=self._container
162+
)
163+
self._ensure_is_running()
164+
165+
return self
166+
167+
def __exit__(self, exc_type: BaseException, exc_value: BaseException, traceback: object) -> None:
168+
self._pod.execute(
169+
command=["pkill", "-f", self._cmd],
170+
)
171+
172+
def is_running(self) -> bool:
173+
out = self._pod.execute(command=["pgrep", "-f", self._cmd], ignore_rc=True)
174+
return bool(out.strip())
175+
176+
@retry(wait_timeout=30, sleep=2, exceptions_dict={})
177+
def _ensure_is_running(self) -> bool:
178+
return self.is_running()
179+
180+
181+
def is_tcp_connection(server: Server, client: Client | PodClient) -> bool:
119182
return server.is_running() and client.is_running()

tests/network/bgp/conftest.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import shlex
23
from collections.abc import Generator
34
from pathlib import Path
45
from typing import Final
@@ -12,10 +13,15 @@
1213
from ocp_resources.pod import Pod
1314

1415
from libs.net import netattachdef as libnad
16+
from libs.net.traffic_generator import PodClient as TcpClient
17+
from libs.net.traffic_generator import Server as TcpServer
1518
from libs.net.udn import create_udn_namespace
19+
from libs.net.vmspec import IP_ADDRESS, lookup_iface_status, lookup_primary_network
20+
from libs.vm.vm import BaseVirtualMachine
1621
from tests.network.libs import cluster_user_defined_network as libcudn
1722
from tests.network.libs import nodenetworkconfigurationpolicy as libnncp
1823
from tests.network.libs.bgp import (
24+
POD_SECONDARY_IFACE_NAME,
1925
create_cudn_route_advertisements,
2026
create_frr_configuration,
2127
deploy_external_frr_pod,
@@ -25,13 +31,16 @@
2531
)
2632
from tests.network.libs.label_selector import LabelSelector
2733
from tests.network.libs.nodenetworkstate import DEFAULT_ROUTE_V4, lookup_br_ex_gateway_v4
34+
from tests.network.libs.vm_factory import udn_vm
2835
from utilities.infra import get_node_selector_dict
2936

3037
APP_CUDN_LABEL: Final[dict] = {"app": "cudn"}
3138
BGP_DATA_PATH: Final[Path] = Path(__file__).resolve().parent / "data" / "frr-config"
3239
CUDN_BGP_LABEL: Final[dict] = {"cudn-bgp": "blue"}
3340
CUDN_SUBNET_IPV4: Final[str] = "192.168.10.0/24"
34-
EXTERNAL_SUBNET_IPV4: Final[str] = "172.100.0.0/16"
41+
EXTERNAL_PROVIDER_SUBNET_IPV4: Final[str] = "10.250.100.0/24"
42+
EXTERNAL_PROVIDER_IP_V4: Final[str] = "10.250.100.150/24"
43+
IPERF3_SERVER_PORT: Final[int] = 2354
3544

3645

3746
@pytest.fixture(scope="session")
@@ -90,7 +99,7 @@ def frr_configmap(
9099
workers: list[Node], cnv_tests_utilities_namespace: Namespace, admin_client: DynamicClient
91100
) -> Generator[ConfigMap]:
92101
frr_conf = generate_frr_conf(
93-
external_subnet_ipv4=EXTERNAL_SUBNET_IPV4,
102+
external_subnet_ipv4=EXTERNAL_PROVIDER_SUBNET_IPV4,
94103
nodes_ipv4_list=[worker.internal_ip for worker in workers],
95104
)
96105

@@ -156,7 +165,7 @@ def frr_configuration_created(admin_client: DynamicClient) -> Generator[None]:
156165
with create_frr_configuration(
157166
name="frr-configuration-bgp",
158167
frr_pod_ipv4=os.environ["EXTERNAL_FRR_STATIC_IPV4"].split("/")[0],
159-
external_subnet_ipv4=EXTERNAL_SUBNET_IPV4,
168+
external_subnet_ipv4=EXTERNAL_PROVIDER_SUBNET_IPV4,
160169
client=admin_client,
161170
):
162171
yield
@@ -179,6 +188,11 @@ def frr_external_pod(
179188
default_route=br_ex_gateway_v4,
180189
client=admin_client,
181190
) as pod:
191+
# Assign a secondary IP on the secondary interface to emulate the external provider subnet
192+
pod.execute(
193+
command=shlex.split(f"ip addr add {EXTERNAL_PROVIDER_IP_V4} dev {POD_SECONDARY_IFACE_NAME}"),
194+
container="frr",
195+
)
182196
yield pod
183197

184198

@@ -191,3 +205,35 @@ def bgp_setup_ready(
191205
) -> None:
192206
node_names = [worker.name for worker in workers]
193207
wait_for_bgp_connection_established(node_names=node_names)
208+
209+
210+
@pytest.fixture(scope="module")
211+
def vm_cudn(
212+
namespace_cudn: Namespace,
213+
cudn_layer2: libcudn.ClusterUserDefinedNetwork,
214+
) -> Generator[BaseVirtualMachine]:
215+
with udn_vm(namespace_name=namespace_cudn.name, name="vm-cudn-bgp") as vm:
216+
vm.start(wait=True)
217+
vm.wait_for_agent_connected()
218+
yield vm
219+
220+
221+
@pytest.fixture(scope="module")
222+
def tcp_server_cudn_vm(vm_cudn: BaseVirtualMachine) -> Generator[TcpServer]:
223+
with TcpServer(vm=vm_cudn, port=IPERF3_SERVER_PORT) as server:
224+
if not server.is_running():
225+
raise ProcessLookupError("Iperf3 server process is not running in the VM")
226+
yield server
227+
228+
229+
@pytest.fixture(scope="module")
230+
def tcp_client_external_network(
231+
frr_external_pod: Pod, vm_cudn: BaseVirtualMachine, tcp_server_cudn_vm: TcpServer
232+
) -> Generator[TcpClient]:
233+
with TcpClient(
234+
pod=frr_external_pod,
235+
server_ip=lookup_iface_status(vm=vm_cudn, iface_name=lookup_primary_network(vm=vm_cudn).name)[IP_ADDRESS],
236+
server_port=IPERF3_SERVER_PORT,
237+
bind_interface=EXTERNAL_PROVIDER_IP_V4.split("/")[0],
238+
) as client:
239+
yield client
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from libs.net.traffic_generator import is_tcp_connection
4+
from utilities.constants import QUARANTINED
5+
from utilities.virt import migrate_vm_and_verify
6+
7+
pytestmark = [pytest.mark.bgp, pytest.mark.usefixtures("bgp_setup_ready")]
8+
9+
10+
@pytest.mark.polarion("CNV-12276")
11+
@pytest.mark.xfail(
12+
reason=f"{QUARANTINED}: BGP test suite infra dependencies are not met, tracked in CNV-69734",
13+
run=False,
14+
)
15+
def test_connectivity_cudn_vm_and_external_network(tcp_server_cudn_vm, tcp_client_external_network):
16+
assert is_tcp_connection(server=tcp_server_cudn_vm, client=tcp_client_external_network)
17+
18+
19+
@pytest.mark.polarion("CNV-12281")
20+
@pytest.mark.xfail(
21+
reason=f"{QUARANTINED}: BGP test suite infra dependencies are not met, tracked in CNV-69734",
22+
run=False,
23+
)
24+
def test_connectivity_is_preserved_during_cudn_vm_migration(
25+
tcp_server_cudn_vm,
26+
tcp_client_external_network,
27+
):
28+
migrate_vm_and_verify(vm=tcp_server_cudn_vm.vm)
29+
assert is_tcp_connection(server=tcp_server_cudn_vm, client=tcp_client_external_network)

tests/network/bgp/test_bgp_infra.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

tests/network/libs/bgp.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
_CLUSTER_FRR_ASN: Final[int] = 64512
2121
_EXTERNAL_FRR_ASN: Final[int] = 64000
2222
_EXTERNAL_FRR_IMAGE: Final[str] = "quay.io/frrouting/frr:9.1.2"
23+
_IPERF3_IMAGE: Final[str] = "quay.io/networkstatic/iperf3"
2324
_FRR_DEPLOYMENT_NAME: Final[str] = "frr-k8s-webhook-server"
2425
_FRR_NS_NAME: Final[str] = "openshift-frr-k8s"
26+
POD_SECONDARY_IFACE_NAME: Final[str] = "net1"
2527

2628

2729
@contextmanager
@@ -184,6 +186,10 @@ def deploy_external_frr_pod(
184186
attaches it to a specified NetworkAttachmentDefinition (NAD), and mounts a ConfigMap for FRR
185187
configuration. On exiting the context, the pod is automatically deleted.
186188
189+
Also contains an iperf3 container to be used for connectivity testing. The process namespace
190+
of the iperf3 container is shared with the frr container for the sake of process management
191+
(due to the minimal capabilities of the iperf3 container).
192+
187193
Args:
188194
namespace_name (str): The name of the namespace where the pod will be deployed.
189195
node_name (str): The name of the node where the pod will be scheduled.
@@ -197,7 +203,7 @@ def deploy_external_frr_pod(
197203
"""
198204
annotations = {
199205
f"{Pod.ApiGroup.K8S_V1_CNI_CNCF_IO}/networks": json.dumps([
200-
{"name": nad_name, "interface": "net1", "default-route": [default_route]}
206+
{"name": nad_name, "interface": POD_SECONDARY_IFACE_NAME, "default-route": [default_route]}
201207
]),
202208
f"{Pod.ApiGroup.K8S_V1_CNI_CNCF_IO}/default-network": "none",
203209
}
@@ -207,7 +213,12 @@ def deploy_external_frr_pod(
207213
"image": _EXTERNAL_FRR_IMAGE,
208214
"securityContext": {"privileged": True, "capabilities": {"add": ["NET_ADMIN"]}},
209215
"volumeMounts": [{"name": frr_configmap_name, "mountPath": "/etc/frr"}],
210-
}
216+
},
217+
{
218+
"name": "iperf3",
219+
"image": _IPERF3_IMAGE,
220+
"command": ["sleep", "infinity"],
221+
},
211222
]
212223
volumes = [{"name": frr_configmap_name, "configMap": {"name": frr_configmap_name}}]
213224

@@ -219,6 +230,7 @@ def deploy_external_frr_pod(
219230
containers=containers,
220231
volumes=volumes,
221232
client=client,
233+
share_process_namespace=True,
222234
) as pod:
223235
pod.wait_for_status(status=Pod.Status.RUNNING)
224236
yield pod

tests/network/libs/vm_factory.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""This module provides various virtual machine configurations with a focus on network setups."""
2+
3+
from libs.net.udn import udn_primary_network
4+
from libs.vm.affinity import new_pod_anti_affinity
5+
from libs.vm.factory import base_vmspec, fedora_vm
6+
from libs.vm.vm import BaseVirtualMachine
7+
8+
9+
def udn_vm(namespace_name: str, name: str, template_labels: dict | None = None) -> BaseVirtualMachine:
10+
spec = base_vmspec()
11+
iface, network = udn_primary_network(name="udn-primary")
12+
spec.template.spec.domain.devices.interfaces = [iface] # type: ignore
13+
spec.template.spec.networks = [network]
14+
if template_labels:
15+
spec.template.metadata.labels = spec.template.metadata.labels or {} # type: ignore
16+
spec.template.metadata.labels.update(template_labels) # type: ignore
17+
# Use the first label key and first value as the anti-affinity label to use:
18+
label, *_ = template_labels.items()
19+
spec.template.spec.affinity = new_pod_anti_affinity(label=label)
20+
21+
return fedora_vm(namespace=namespace_name, name=name, spec=spec)

tests/network/user_defined_network/test_user_defined_network.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
from ocp_resources.utils.constants import TIMEOUT_1MINUTE
66

77
from libs.net.traffic_generator import Client, Server, is_tcp_connection
8-
from libs.net.udn import udn_primary_network
98
from libs.net.vmspec import lookup_iface_status, lookup_primary_network
109
from libs.vm import affinity
11-
from libs.vm.affinity import new_pod_anti_affinity
12-
from libs.vm.factory import base_vmspec, fedora_vm
10+
from tests.network.libs.vm_factory import udn_vm
1311
from utilities.constants import PUBLIC_DNS_SERVER_IP, QUARANTINED, TIMEOUT_1MIN
1412
from utilities.infra import create_ns
1513
from utilities.virt import migrate_vm_and_verify
@@ -18,21 +16,6 @@
1816
SERVER_PORT = 5201
1917

2018

21-
def udn_vm(namespace_name, name, template_labels=None):
22-
spec = base_vmspec()
23-
iface, network = udn_primary_network(name="udn-primary")
24-
spec.template.spec.domain.devices.interfaces = [iface]
25-
spec.template.spec.networks = [network]
26-
if template_labels:
27-
spec.template.metadata.labels = spec.template.metadata.labels or {}
28-
spec.template.metadata.labels.update(template_labels)
29-
# Use the first label key and first value as the anti-affinity label to use:
30-
label, *_ = template_labels.items()
31-
spec.template.spec.affinity = new_pod_anti_affinity(label=label)
32-
33-
return fedora_vm(namespace=namespace_name, name=name, spec=spec)
34-
35-
3619
@pytest.fixture(scope="module")
3720
def udn_namespace(admin_client):
3821
yield from create_ns(

0 commit comments

Comments
 (0)