Skip to content

Add IPv6 support in NetworkInterface feature #3957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lisa/features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@
from .infiniband import Infiniband
from .isolated_resource import IsolatedResource
from .nested_virtualization import NestedVirtualization
from .network_interface import NetworkInterface, Sriov, Synthetic
from .network_interface import (
NetworkInterface,
Sriov,
SriovIPv6,
Synthetic,
SyntheticIPv6,
)
from .nfs import Nfs
from .nvme import Nvme, NvmeSettings
from .password_extension import PasswordExtension
Expand Down Expand Up @@ -69,6 +75,8 @@
"SecurityProfileSettings",
"SecurityProfileType",
"Sriov",
"SriovIPv6",
"SyntheticIPv6",
"StopState",
"VMStatus",
"Synthetic",
Expand Down
10 changes: 10 additions & 0 deletions lisa/features/network_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,13 @@ def _initialize(self, *args: Any, **kwargs: Any) -> None:
Synthetic = partial(
NetworkInterfaceOptionSettings, data_path=schema.NetworkDataPath.Synthetic
)
SriovIPv6 = partial(
NetworkInterfaceOptionSettings,
data_path=schema.NetworkDataPath.Sriov,
ip_version=schema.IPVersion.IPv6,
)
SyntheticIPv6 = partial(
NetworkInterfaceOptionSettings,
data_path=schema.NetworkDataPath.Synthetic,
ip_version=schema.IPVersion.IPv6,
)
31 changes: 29 additions & 2 deletions lisa/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,11 @@ class NetworkDataPath(str, Enum):
Sriov = "Sriov"


class IPVersion(str, Enum):
IPv4 = "IPv4"
IPv6 = "IPv6"


_network_data_path_priority: List[NetworkDataPath] = [
NetworkDataPath.Sriov,
NetworkDataPath.Synthetic,
Expand Down Expand Up @@ -740,6 +745,21 @@ class NetworkInterfaceOptionSettings(FeatureSettings):
)
),
)
ip_version: Optional[Union[search_space.SetSpace[IPVersion], IPVersion]] = (
field( # type: ignore
default_factory=partial(
search_space.SetSpace,
items=[IPVersion.IPv4, IPVersion.IPv6],
),
metadata=field_metadata(
decoder=partial(
search_space.decode_nullable_set_space,
base_type=IPVersion,
default_values=[IPVersion.IPv4, IPVersion.IPv6],
)
),
)
)
# nic_count is used for specifying associated nic count during provisioning vm
nic_count: search_space.CountSpace = field(
default_factory=partial(search_space.IntRange, min=1),
Expand All @@ -762,14 +782,15 @@ def __eq__(self, o: object) -> bool:
return (
self.type == o.type
and self.data_path == o.data_path
and self.ip_version == o.ip_version
and self.nic_count == o.nic_count
and self.max_nic_count == o.max_nic_count
)

def __repr__(self) -> str:
return (
f"data_path:{self.data_path}, nic_count:{self.nic_count},"
f" max_nic_count:{self.max_nic_count}"
f"data_path:{self.data_path}, ip_version:{self.ip_version}, "
f"nic_count:{self.nic_count}, max_nic_count:{self.max_nic_count}"
)

def __str__(self) -> str:
Expand Down Expand Up @@ -806,6 +827,11 @@ def check(self, capability: Any) -> search_space.ResultReason:
"data_path",
)

result.merge(
search_space.check_setspace(self.ip_version, capability.ip_version),
"ip_version",
)

result.merge(
search_space.check_countspace(self.max_nic_count, capability.max_nic_count),
"max_nic_count",
Expand Down Expand Up @@ -839,6 +865,7 @@ def _call_requirement_method(
value.data_path = getattr(search_space, f"{method.value}_setspace_by_priority")(
self.data_path, capability.data_path, _network_data_path_priority
)

return value


Expand Down
23 changes: 23 additions & 0 deletions lisa/sut_orchestrator/azure/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,9 +783,32 @@ class NetworkInterface(AzureFeatureMixin, features.NetworkInterface):
def settings_type(cls) -> Type[schema.FeatureSettings]:
return schema.NetworkInterfaceOptionSettings

@classmethod
def create_setting(
cls, *args: Any, **kwargs: Any
) -> Optional[schema.FeatureSettings]:
# All Azure VMs support synthetic and SRIOV networking
# All Azure VMs can support both IPv4 and IPv6 via ARM template configuration
return schema.NetworkInterfaceOptionSettings(
data_path=search_space.SetSpace(
items=[
schema.NetworkDataPath.Synthetic,
schema.NetworkDataPath.Sriov,
]
),
ip_version=search_space.SetSpace(
is_allow_set=True,
items=[
schema.IPVersion.IPv4,
schema.IPVersion.IPv6,
],
),
)

def _initialize(self, *args: Any, **kwargs: Any) -> None:
super()._initialize(*args, **kwargs)
self._initialize_information(self._node)

all_nics = self._get_all_nics()
# store extra synthetic and sriov nics count
# in order to restore nics status after testing which needs change nics
Expand Down
34 changes: 34 additions & 0 deletions lisa/sut_orchestrator/azure/platform_.py
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,12 @@ def _create_deployment_parameters(
)
arm_parameters.use_ipv6 = self._azure_runbook.use_ipv6

# Override IPv6 setting if environment specifically requires IPv6
if not arm_parameters.use_ipv6: # Only check if not already enabled
ipv6_required = self._check_ipv6_requirements(environment)
if ipv6_required:
arm_parameters.use_ipv6 = True

is_windows: bool = False
arm_parameters.admin_username = self.runbook.admin_username
# if no key or password specified, generate the key pair
Expand Down Expand Up @@ -2242,6 +2248,11 @@ def _generate_max_capability(self, vm_size: str, location: str) -> AzureCapabili
](is_allow_set=True, items=[])
node_space.network_interface.data_path.add(schema.NetworkDataPath.Synthetic)
node_space.network_interface.data_path.add(schema.NetworkDataPath.Sriov)
node_space.network_interface.ip_version = search_space.SetSpace[
schema.IPVersion
](is_allow_set=True, items=[])
node_space.network_interface.ip_version.add(schema.IPVersion.IPv4)
node_space.network_interface.ip_version.add(schema.IPVersion.IPv6)
node_space.network_interface.nic_count = search_space.IntRange(min=1)
# till now, the max nic number supported in Azure is 8
node_space.network_interface.max_nic_count = 8
Expand Down Expand Up @@ -3054,6 +3065,29 @@ def _is_byoip_feature_registered(self) -> bool:
self._cached_byoip_registered = False
return self._cached_byoip_registered

def _check_ipv6_requirements(self, environment: Environment) -> bool:
"""Check if IPv6 is specifically required by analyzing environment requirements."""
ipv6_required = False

# Check environment runbook node requirements for IPv6 specifications
if environment.runbook and environment.runbook.nodes_requirement:
for node_requirement in environment.runbook.nodes_requirement:
if (
node_requirement.network_interface
and node_requirement.network_interface.ip_version is not None
):
ip_version = node_requirement.network_interface.ip_version
if isinstance(ip_version, search_space.SetSpace):
if schema.IPVersion.IPv6 in ip_version.items:
ipv6_required = True
break
elif isinstance(ip_version, schema.IPVersion):
if ip_version == schema.IPVersion.IPv6:
ipv6_required = True
break

return ipv6_required


def _get_allowed_locations(nodes_requirement: List[schema.NodeSpace]) -> List[str]:
existing_locations_str: str = ""
Expand Down
13 changes: 12 additions & 1 deletion lisa/tools/ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from lisa.tools import Cat
from lisa.tools.start_configuration import StartConfiguration
from lisa.tools.whoami import Whoami
from lisa.util import LisaException, find_patterns_in_lines
from lisa.util import LisaException, find_patterns_in_lines, get_matched_str


class IpInfo:
Expand Down Expand Up @@ -346,6 +346,17 @@ def get_ip_address(self, nic_name: str) -> str:
assert "ip_addr" in matched, f"not find ip address for nic {nic_name}"
return matched["ip_addr"]

def get_ipv6_address(self, nic_name: str) -> str:
"""Get the global IPv6 address for a network interface."""
result = self.run(f"-6 addr show {nic_name}", force_run=True, sudo=True)

# Regex to match IPv6 addresses with global scope
# Example: inet6 2001:db8::5/128 scope global dynamic noprefixroute
ipv6_pattern = re.compile(r"inet6\s+([0-9a-fA-F:]+)\/\d+\s+scope\s+global")

ipv6_address = get_matched_str(result.stdout, ipv6_pattern)
return ipv6_address

def get_default_route_info(self) -> tuple[str, str]:
result = self.run("route", force_run=True, sudo=True)
result.assert_exit_code()
Expand Down
11 changes: 9 additions & 2 deletions lisa/tools/lagscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,17 @@ def restore_busy_poll(self) -> None:
for key in self._busy_pool_keys:
sysctl.write(key, self._original_settings[key])

def run_as_server_async(self, ip: str = "") -> Process:
def run_as_server_async(self, ip: str = "", use_ipv6: bool = False) -> Process:
# -r: run as a receiver
# -rip: run as server mode with specified ip address
cmd = ""
if use_ipv6:
cmd += " -6"
if ip:
cmd += f" -r{ip}"
else:
cmd += " -r"

process = self.run_async(cmd, sudo=True, shell=True, force_run=True)
if not process.is_running():
raise LisaException("lagscope server failed to start")
Expand All @@ -159,6 +162,7 @@ def run_as_client_async(
count_of_histogram_intervals: int = 30,
dump_csv: bool = True,
daemon: bool = False,
use_ipv6: bool = False,
) -> Process:
# -s: run as a sender
# -i: test interval
Expand All @@ -172,6 +176,8 @@ def run_as_client_async(
# -R: dumps raw latencies into csv file
# -D: run as daemon
cmd = f"{self.command} -s{server_ip} "
if use_ipv6:
cmd += " -6 "
if run_time_seconds:
cmd += f" -t{run_time_seconds} "
if count_of_histogram_intervals:
Expand Down Expand Up @@ -391,7 +397,7 @@ def restore_busy_poll(self) -> None:
# This is not supported on FreeBSD.
return

def run_as_server_async(self, ip: str = "") -> Process:
def run_as_server_async(self, ip: str = "", use_ipv6: bool = False) -> Process:
return self.node.tools[Sockperf].start_server_async("tcp")

def run_as_client_async(
Expand All @@ -407,6 +413,7 @@ def run_as_client_async(
count_of_histogram_intervals: int = 30,
dump_csv: bool = True,
daemon: bool = False,
use_ipv6: bool = False,
) -> Process:
return self.node.tools[Sockperf].run_client_async("tcp", server_ip)

Expand Down
10 changes: 9 additions & 1 deletion lisa/tools/ntttcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,11 @@ def run_as_server_async(
dev_differentiator: str = "Hypervisor callback interrupts",
run_as_daemon: bool = False,
udp_mode: bool = False,
use_ipv6: bool = False,
) -> Process:
cmd = ""
if use_ipv6:
cmd += " -6 "
if server_ip:
cmd += f" -r{server_ip} "
cmd += (
Expand Down Expand Up @@ -308,6 +311,7 @@ def run_as_client(
dev_differentiator: str = "Hypervisor callback interrupts",
run_as_daemon: bool = False,
udp_mode: bool = False,
use_ipv6: bool = False,
) -> ExecutableResult:
# -sserver_ip: run as a sender with server ip address
# -P: Number of ports listening on receiver side [default: 16] [max: 512]
Expand All @@ -326,11 +330,15 @@ def run_as_client(
# the devices specified by the differentiator
# Examples for differentiator: Hyper-V PCIe MSI, mlx4, Hypervisor callback
# interrupts
cmd = (
cmd = ""
if use_ipv6:
cmd += " -6 "
cmd += (
f" -s{server_ip} -P {ports_count} -n {threads_count} -t {run_time_seconds} "
f"-W {warm_up_time_seconds} -C {cool_down_time_seconds} -b {buffer_size}k "
f"--show-nic-packets {nic_name} "
)

if udp_mode:
cmd += " -u "
if dev_differentiator:
Expand Down
24 changes: 24 additions & 0 deletions microsoft/testsuites/network/sriov.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
Lscpu,
Service,
)
from lisa.tools.ip import Ip
from lisa.util import (
LisaException,
LisaTimeoutException,
Expand Down Expand Up @@ -877,6 +878,29 @@ def verify_sriov_interrupts_change(self, environment: Environment) -> None:
"interrupt count!"
).is_greater_than(unused_cpu)

@TestCaseMetadata(
description="""
Verify IPv6 networking functionality with IPv6
1. Create an Azure VM with AN Enabled and IPv6 enabled
2. Verify that the NIC has an IPv6 address
""",
priority=2,
requirement=node_requirement(
node=schema.NodeSpace(
network_interface=features.SriovIPv6(),
)
),
)
def verify_sriov_ipv6_basic(self, node: Node, log: Logger) -> None:
ip = node.tools[Ip]
# for each nic, verify that ipv6 exists using nic name
for nic in node.nics.nics.keys():
nic_ipv6 = ip.get_ipv6_address(nic)
assert_that(
nic_ipv6,
f"Expected IPv6 address but found none on nic {nic}",
).is_not_empty()

def after_case(self, log: Logger, **kwargs: Any) -> None:
environment: Environment = kwargs.pop("environment")
cleanup_iperf3(environment)
25 changes: 25 additions & 0 deletions microsoft/testsuites/network/synthetic.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
from logging import Logger

from assertpy import assert_that

from lisa import (
Environment,
TestCaseMetadata,
TestSuite,
TestSuiteMetadata,
features,
schema,
simple_requirement,
)
from lisa.features import NetworkInterface, StartStop
from lisa.node import Node
from lisa.tools.ip import Ip

from .common import initialize_nic_info, remove_extra_nics, restore_extra_nics

Expand Down Expand Up @@ -191,3 +198,21 @@ def verify_synthetic_add_max_nics_one_by_one_after_provision(
initialize_nic_info(environment, is_sriov=False)
finally:
restore_extra_nics(environment)

@TestCaseMetadata(
"",
priority=2,
use_new_environment=True,
requirement=simple_requirement(
network_interface=features.SyntheticIPv6(),
),
)
def verify_synthetic_ipv6_basic(self, node: Node, log: Logger) -> None:
ip = node.tools[Ip]
# for each nic, verify that ipv6 exists using nic name
for nic in node.nics.nics.keys():
nic_ipv6 = ip.get_ipv6_address(nic)
assert_that(
nic_ipv6,
f"Expected IPv6 address but found none on nic {nic}",
).is_not_empty()
Loading
Loading