Skip to content
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it reuse IpProtocol.ipv6 in other places, instead of creating a new 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
16 changes: 15 additions & 1 deletion lisa/sut_orchestrator/azure/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2274,13 +2274,27 @@ def get_primary_ip_addresses(
nic_name = get_matched_str(network_interface.id, NIC_NAME_PATTERN)
nic = network_client.network_interfaces.get(resource_group_name, nic_name)
if nic.primary:
# Check if IPv6 is actually enabled on the NIC
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which setting determines whether IpProtocol.ipv6 is enabled?

# if its enabled, use it as internal ip.
ipv6_enabled = any(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between this value and use_ipv6?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the problem I was having.
use_ipv6 is currently passed by runbook. This flow makes both internal address and public address as ipv6.
My requirement changes to

  1. Ipv4 as public
  2. Ipv6 as internal
  3. IPv6 requirement is a feature rather than runbook parameter.

Can you suggest a better approach for the above?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I found four instances of use_ipv6 in RemoteNode, AzurePlatformSchema, AzureArmParameter, and NodeContext, but only AzurePlatformSchema and AzureArmParameter appear to be used. Please review the code history to understand the purpose of the other two.
  2. To support the scenario you mentioned, please update the code to define use_ipv6_public and use_ipv6_internal, both defaulting to False.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, let me rework the PR

ip_config.private_ip_address_version == IpProtocol.ipv6
for ip_config in nic.ip_configurations
)
if use_ipv6:
nic_index = 1

ip_config = nic.ip_configurations[nic_index]
if use_ipv6 and ip_config.private_ip_address_version != IpProtocol.ipv6:
raise LisaException(f"private address is not IPv6 in nic {nic.name}")
private_ip = ip_config.private_ip_address

# If IPv6 is actually enabled on the NIC, use it as the internal IP.
# Public IP can continue to be IPv4.
if ipv6_enabled:
ipv6_nic_index = 1
ipv6_config = nic.ip_configurations[ipv6_nic_index]
private_ip = ipv6_config.private_ip_address
else:
private_ip = ip_config.private_ip_address

if create_public_address:
if not ip_config.public_ip_address:
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this method used? There wasn’t such a method, but the SR-IOV and synthetic settings worked correctly.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overrides of requirements should be defined in the runbook, and the runbook requirements are merged with the test requirements in lisa_runner. The Azure Platform shouldn't modify it, as that would disrupt the flow.

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a parameter to the get_ip_address method instead of creating a new method.

"""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
6 changes: 6 additions & 0 deletions lisa/tools/lagscope.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import ipaddress
import re
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, cast
Expand Down Expand Up @@ -135,10 +136,13 @@ def run_as_server_async(self, ip: str = "") -> Process:
# -r: run as a receiver
# -rip: run as server mode with specified ip address
cmd = ""
if ipaddress.ip_address(ip).version == 6:
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 Down Expand Up @@ -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 ipaddress.ip_address(server_ip).version == 6:
cmd += " -6 "
if run_time_seconds:
cmd += f" -t{run_time_seconds} "
if count_of_histogram_intervals:
Expand Down
9 changes: 8 additions & 1 deletion lisa/tools/ntttcp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import ipaddress
import re
import time
from decimal import Decimal
Expand Down Expand Up @@ -215,6 +216,8 @@ def run_as_server_async(
udp_mode: bool = False,
) -> Process:
cmd = ""
if ipaddress.ip_address(server_ip).version == 6:
cmd += " -6 "
if server_ip:
cmd += f" -r{server_ip} "
cmd += (
Expand Down Expand Up @@ -326,11 +329,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 ipaddress.ip_address(server_ip).version == 6:
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)
Loading
Loading