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: 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
)
SriovIpv6Internal = partial(
NetworkInterfaceOptionSettings,
data_path=schema.NetworkDataPath.Sriov,
ip_version=schema.IpProtocol.ipv6,
)
SyntheticIpv6Internal = partial(
NetworkInterfaceOptionSettings,
data_path=schema.NetworkDataPath.Synthetic,
ip_version=schema.IpProtocol.ipv6,
)
21 changes: 20 additions & 1 deletion 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 IpProtocol(str, Enum):
ipv4 = "IPv4"
ipv6 = "IPv6"


_network_data_path_priority: List[NetworkDataPath] = [
NetworkDataPath.Sriov,
NetworkDataPath.Synthetic,
Expand Down Expand Up @@ -753,6 +758,21 @@ class NetworkInterfaceOptionSettings(FeatureSettings):
allow_none=True, decoder=search_space.decode_count_space
),
)
ip_version: Optional[Union[search_space.SetSpace[IpProtocol], IpProtocol]] = field(
default_factory=partial(
search_space.SetSpace,
items=[IpProtocol.ipv4, IpProtocol.ipv6],
),
metadata=field_metadata(
decoder=partial(
search_space.decode_nullable_set_space,
base_type=IpProtocol,
default_values=[IpProtocol.ipv4, IpProtocol.ipv6],
),
required=False,
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 be set to empty? And if so, what happens when it is?

allow_none=True,
),
)

def __eq__(self, o: object) -> bool:
if not super().__eq__(o):
Expand Down Expand Up @@ -1314,7 +1334,6 @@ class RemoteNode(Node):
username: str = constants.DEFAULT_USER_NAME
password: str = ""
private_key_file: str = ""
use_ipv6: bool = False

def __post_init__(self, *args: Any, **kwargs: Any) -> None:
add_secret(self.username, PATTERN_HEADTAIL)
Expand Down
16 changes: 12 additions & 4 deletions lisa/sut_orchestrator/azure/arm_template.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ param is_ultradisk bool = false
@description('IP Service Tags')
param ip_service_tags object

@description('whether to use ipv6')
@description('whether to use ipv6 (legacy parameter for backward compatibility)')
Copy link
Member

Choose a reason for hiding this comment

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

Did you find any usages internally? If so, create a PR to main-ci branch to fix them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I could only find one reference in lisa/microsoft/runbook/azure.yml

Copy link
Member

Choose a reason for hiding this comment

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

Ok, please remove it.

param use_ipv6 bool = false

@description('whether to use ipv6 for public addresses')
param use_ipv6_public bool = false

@description('whether to use ipv6 for internal/private addresses')
param use_ipv6_internal bool = false

@description('whether to enable network outbound access')
param enable_vm_nat bool

Expand Down Expand Up @@ -233,6 +239,8 @@ module nodes_nics './nested_nodes_nics.bicep' = [for i in range(0, node_count):
enable_sriov: nodes[i].enable_sriov
tags: tags
use_ipv6: use_ipv6
use_ipv6_public: use_ipv6_public
use_ipv6_internal: use_ipv6_internal
create_public_address: create_public_address
}
dependsOn: [
Expand All @@ -249,15 +257,15 @@ resource virtual_network_name_resource 'Microsoft.Network/virtualNetworks@2024-0
addressSpace: {
addressPrefixes: concat(
['10.0.0.0/16'],
use_ipv6 ? ['2001:db8::/32'] : []
(use_ipv6 || use_ipv6_internal) ? ['2001:db8::/32'] : []
)
}
subnets: [for j in range(0, subnet_count): {
name: '${subnet_prefix}${j}'
properties: {
addressPrefixes: concat(
['10.0.${j}.0/24'],
use_ipv6 ? ['2001:db8:${j}::/64'] : []
(use_ipv6 || use_ipv6_internal) ? ['2001:db8:${j}::/64'] : []
)
defaultOutboundAccess: enable_vm_nat
networkSecurityGroup: {
Expand Down Expand Up @@ -345,7 +353,7 @@ resource nodes_public_ip 'Microsoft.Network/publicIPAddresses@2020-05-01' = [for
zones: (use_availability_zones ? availability_zones : null)
}]

resource nodes_public_ip_ipv6 'Microsoft.Network/publicIPAddresses@2020-05-01' = [for i in range(0, node_count): if (use_ipv6 && create_public_address) {
resource nodes_public_ip_ipv6 'Microsoft.Network/publicIPAddresses@2020-05-01' = [for i in range(0, node_count): if ((use_ipv6 || use_ipv6_public) && create_public_address) {
name: '${nodes[i].name}-public-ipv6'
location: location
tags: tags
Expand Down
40 changes: 33 additions & 7 deletions lisa/sut_orchestrator/azure/autogen_arm_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"_generator": {
"name": "bicep",
"version": "0.37.4.10188",
"templateHash": "16291477761159155605"
"templateHash": "7375950074177020167"
}
},
"functions": [
Expand Down Expand Up @@ -522,7 +522,21 @@
"type": "bool",
"defaultValue": false,
"metadata": {
"description": "whether to use ipv6"
"description": "whether to use ipv6 (legacy parameter for backward compatibility)"
}
},
"use_ipv6_public": {
"type": "bool",
"defaultValue": false,
"metadata": {
"description": "whether to use ipv6 for public IP addresses"
}
},
"use_ipv6_internal": {
"type": "bool",
"defaultValue": false,
"metadata": {
"description": "whether to use ipv6 for internal IP addresses"
}
},
"enable_vm_nat": {
Expand Down Expand Up @@ -585,7 +599,7 @@
"input": {
"name": "[format('{0}{1}', parameters('subnet_prefix'), range(0, parameters('subnet_count'))[copyIndex('subnets')])]",
"properties": {
"addressPrefixes": "[concat(createArray(format('10.0.{0}.0/24', range(0, parameters('subnet_count'))[copyIndex('subnets')])), if(parameters('use_ipv6'), createArray(format('2001:db8:{0}::/64', range(0, parameters('subnet_count'))[copyIndex('subnets')])), createArray()))]",
"addressPrefixes": "[concat(createArray(format('10.0.{0}.0/24', range(0, parameters('subnet_count'))[copyIndex('subnets')])), if(or(parameters('use_ipv6'), parameters('use_ipv6_internal')), createArray(format('2001:db8:{0}::/64', range(0, parameters('subnet_count'))[copyIndex('subnets')])), createArray()))]",
"defaultOutboundAccess": "[parameters('enable_vm_nat')]",
"networkSecurityGroup": {
"id": "[resourceId('Microsoft.Network/networkSecurityGroups', format('{0}-nsg', toLower(parameters('virtual_network_name'))))]"
Expand All @@ -595,7 +609,7 @@
}
],
"addressSpace": {
"addressPrefixes": "[concat(createArray('10.0.0.0/16'), if(parameters('use_ipv6'), createArray('2001:db8::/32'), createArray()))]"
"addressPrefixes": "[concat(createArray('10.0.0.0/16'), if(or(parameters('use_ipv6'), parameters('use_ipv6_internal')), createArray('2001:db8::/32'), createArray()))]"
}
},
"dependsOn": [
Expand Down Expand Up @@ -690,7 +704,7 @@
"name": "nodes_public_ip_ipv6",
"count": "[length(range(0, variables('node_count')))]"
},
"condition": "[and(parameters('use_ipv6'), parameters('create_public_address'))]",
"condition": "[and(or(parameters('use_ipv6'), parameters('use_ipv6_public')), parameters('create_public_address'))]",
"type": "Microsoft.Network/publicIPAddresses",
"apiVersion": "2020-05-01",
"name": "[format('{0}-public-ipv6', parameters('nodes')[range(0, variables('node_count'))[copyIndex()]].name)]",
Expand Down Expand Up @@ -887,6 +901,12 @@
"use_ipv6": {
"value": "[parameters('use_ipv6')]"
},
"use_ipv6_public": {
"value": "[parameters('use_ipv6_public')]"
},
"use_ipv6_internal": {
"value": "[parameters('use_ipv6_internal')]"
},
"create_public_address": {
"value": "[parameters('create_public_address')]"
}
Expand All @@ -899,7 +919,7 @@
"_generator": {
"name": "bicep",
"version": "0.37.4.10188",
"templateHash": "17932923534203698710"
"templateHash": "797006272337831175"
}
},
"functions": [
Expand Down Expand Up @@ -955,6 +975,12 @@
"use_ipv6": {
"type": "bool"
},
"use_ipv6_public": {
"type": "bool"
},
"use_ipv6_internal": {
"type": "bool"
},
"create_public_address": {
"type": "bool"
}
Expand All @@ -975,7 +1001,7 @@
"location": "[parameters('location')]",
"tags": "[parameters('tags')]",
"properties": {
"ipConfigurations": "[concat(createArray(createObject('name', 'IPv4Config', 'properties', createObject('privateIPAddressVersion', 'IPv4', 'publicIPAddress', if(and(equals(0, range(0, parameters('nic_count'))[copyIndex()]), parameters('create_public_address')), variables('publicIpAddress'), null()), 'subnet', createObject('id', if(not(empty(parameters('existing_subnet_ref'))), parameters('existing_subnet_ref'), format('{0}/subnets/{1}{2}', parameters('vnet_id'), parameters('subnet_prefix'), range(0, parameters('nic_count'))[copyIndex()]))), 'privateIPAllocationMethod', 'Dynamic'))), if(parameters('use_ipv6'), createArray(createObject('name', 'IPv6Config', 'properties', createObject('privateIPAddressVersion', 'IPv6', 'publicIPAddress', if(and(equals(0, range(0, parameters('nic_count'))[copyIndex()]), parameters('create_public_address')), variables('publicIpAddressV6'), null()), 'subnet', createObject('id', if(not(empty(parameters('existing_subnet_ref'))), parameters('existing_subnet_ref'), format('{0}/subnets/{1}{2}', parameters('vnet_id'), parameters('subnet_prefix'), range(0, parameters('nic_count'))[copyIndex()]))), 'privateIPAllocationMethod', 'Dynamic'))), createArray()))]",
"ipConfigurations": "[concat(createArray(createObject('name', 'IPv4Config', 'properties', createObject('privateIPAddressVersion', 'IPv4', 'publicIPAddress', if(and(equals(0, range(0, parameters('nic_count'))[copyIndex()]), parameters('create_public_address')), variables('publicIpAddress'), null()), 'subnet', createObject('id', if(not(empty(parameters('existing_subnet_ref'))), parameters('existing_subnet_ref'), format('{0}/subnets/{1}{2}', parameters('vnet_id'), parameters('subnet_prefix'), range(0, parameters('nic_count'))[copyIndex()]))), 'privateIPAllocationMethod', 'Dynamic'))), if(or(parameters('use_ipv6'), parameters('use_ipv6_internal')), createArray(createObject('name', 'IPv6Config', 'properties', createObject('privateIPAddressVersion', 'IPv6', 'publicIPAddress', if(and(and(equals(0, range(0, parameters('nic_count'))[copyIndex()]), parameters('create_public_address')), or(parameters('use_ipv6'), parameters('use_ipv6_public'))), variables('publicIpAddressV6'), null()), 'subnet', createObject('id', if(not(empty(parameters('existing_subnet_ref'))), parameters('existing_subnet_ref'), format('{0}/subnets/{1}{2}', parameters('vnet_id'), parameters('subnet_prefix'), range(0, parameters('nic_count'))[copyIndex()]))), 'privateIPAllocationMethod', 'Dynamic'))), createArray()))]",
"enableAcceleratedNetworking": "[parameters('enable_sriov')]"
}
}
Expand Down
15 changes: 8 additions & 7 deletions lisa/sut_orchestrator/azure/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,11 +1126,6 @@ class DiskPlacementType(str, Enum):
NVME = "NvmeDisk"


class IpProtocol(str, Enum):
ipv4 = "IPv4"
ipv6 = "IPv6"


def get_disk_placement_priority() -> List[DiskPlacementType]:
return [
DiskPlacementType.NVME,
Expand Down Expand Up @@ -1214,6 +1209,8 @@ class AzureArmParameter:
subnet_prefix: str = AZURE_SUBNET_PREFIX
is_ultradisk: bool = False
use_ipv6: bool = False
use_ipv6_public: bool = False
use_ipv6_internal: bool = False
enable_vm_nat: bool = False
create_public_address: bool = True
source_address_prefixes: List[str] = field(default_factory=list)
Expand Down Expand Up @@ -2274,7 +2271,10 @@ def get_primary_ip_addresses(
nic_index = 1

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

Expand All @@ -2291,7 +2291,8 @@ def get_primary_ip_addresses(
)
if (
use_ipv6
and public_ip_address.public_ip_address_version != IpProtocol.ipv6
and public_ip_address.public_ip_address_version
!= schema.IpProtocol.ipv6
):
raise LisaException(f"public address is not IPv6 in nic {nic.name}")

Expand Down
6 changes: 4 additions & 2 deletions lisa/sut_orchestrator/azure/nested_nodes_nics.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ param existing_subnet_ref string
param enable_sriov bool
param tags object
param use_ipv6 bool
param use_ipv6_public bool = false
param use_ipv6_internal bool = false
param create_public_address bool

func getPublicIpAddress(vmName string, publicIpName string) object => {
Expand Down Expand Up @@ -35,12 +37,12 @@ resource vm_nics 'Microsoft.Network/networkInterfaces@2023-06-01' = [for i in ra
}
}
],
use_ipv6 ? [
(use_ipv6 || use_ipv6_internal) ? [
{
name: 'IPv6Config'
properties: {
privateIPAddressVersion: 'IPv6'
publicIPAddress: ((0 == i && create_public_address) ? publicIpAddressV6 : null)
publicIPAddress: ((0 == i && create_public_address && (use_ipv6 || use_ipv6_public)) ? publicIpAddressV6 : null)
subnet: {
id: ((!empty(existing_subnet_ref)) ? existing_subnet_ref : '${vnet_id}/subnets/${subnet_prefix}${i}')
}
Expand Down
27 changes: 27 additions & 0 deletions lisa/sut_orchestrator/azure/platform_.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,11 @@ class AzurePlatformSchema:
tags: Optional[Dict[str, Any]] = field(default=None)
use_public_address: bool = field(default=True)
create_public_address: bool = field(default=True)
# use_ipv6 should be deprecated, use use_ipv6_internal
# and use_ipv6_public instead
use_ipv6: bool = field(default=False)
use_ipv6_internal: bool = field(default=False)
use_ipv6_public: bool = field(default=False)
ip_service_tags: Optional[Dict[str, str]] = field(default=None)
# Default outbound access is disabled for better security and control.
# As of September 30, 2025, default outbound access for new deployments
Expand Down Expand Up @@ -376,6 +380,8 @@ def __post_init__(self, *args: Any, **kwargs: Any) -> None:
"subnet_prefix",
"use_public_address",
"use_ipv6",
"use_ipv6_public",
"use_ipv6_internal",
"enable_vm_nat",
"source_address_prefixes",
"create_public_address",
Expand Down Expand Up @@ -1211,6 +1217,8 @@ def _create_deployment_parameters(
self._azure_runbook.virtual_network_name or AZURE_VIRTUAL_NETWORK_NAME
)
arm_parameters.use_ipv6 = self._azure_runbook.use_ipv6
arm_parameters.use_ipv6_public = self._azure_runbook.use_ipv6_public
arm_parameters.use_ipv6_internal = self._azure_runbook.use_ipv6_internal

is_windows: bool = False
arm_parameters.admin_username = self.runbook.admin_username
Expand Down Expand Up @@ -1307,6 +1315,25 @@ def _create_deployment_parameters(
if f.type not in features_settings:
features_settings[f.type] = f

# Check for IPv6 network interface requirements and set deployment flags
if (
node.capability.network_interface
and hasattr(node.capability.network_interface, 'ip_version')
Copy link
Member

Choose a reason for hiding this comment

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

Why bother checking if the attribute exists? It’s supposed to be there by default.

and node.capability.network_interface.ip_version is not None
):
# Handle both single value and SetSpace cases
ip_version = node.capability.network_interface.ip_version
if hasattr(ip_version, 'items'):
Copy link
Member

@squirrelsc squirrelsc Oct 6, 2025

Choose a reason for hiding this comment

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

Instead of inspecting attributes, just verify the type directly. It'll keep things cleaner and more straightforward. Use def generate_min_capability_setspace_by_priority with ipv6 to check if ipv6 exists or not.

# SetSpace case - check if IPv6 is in the set
ipv6_required = schema.IpProtocol.ipv6 in ip_version.items
Copy link
Member

Choose a reason for hiding this comment

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

Why ipv6 is here, and so it would be required?

else:
# Single value case
ipv6_required = ip_version == schema.IpProtocol.ipv6

if ipv6_required:
log.info("IPv6 network interface requirement detected, enabling IPv6 internal")
arm_parameters.use_ipv6_internal = True
Copy link
Member

Choose a reason for hiding this comment

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

how about use_ipv6_public? Does it needs to be set to true?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You are right, currently I am not checking for public.
Let me recheck this logic

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I'll need to add the below to class NetworkInterfaceOptionSettings(FeatureSettings):

ipv6_addressing_type: Optional[
    Union[search_space.SetSpace[IPv6AddressingType], IPv6AddressingType]
] = field(
    default=None,  # Default to None when IPv4 is used
    metadata=field_metadata(
        decoder=partial(
            search_space.decode_nullable_set_space,
            base_type=IPv6AddressingType,
            default_values=[
                IPv6AddressingType.internal_only,
                IPv6AddressingType.public_and_internal,
                IPv6AddressingType.public_only,
            ],  # Capabilities can support all types
        ),
        required=False,
        allow_none=True,
    ),
)

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 purpose of this setting?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was thinking of replacing ip_version: Optional[Union[search_space.SetSpace[IpProtocol], IpProtocol]] with ipv6_addressing_type.
ipv6_addressing_type if none = ipv6 disabled

Otherwise, there is no way to determine if ipv6 is required for internal, public or both

Copy link
Member

Choose a reason for hiding this comment

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

It looks like we need to follow the design for use_ipv6_public and use_ipv6_internal at the feature level. But in the Bicep code, they should be named enable_ipv6_public and enable_ipv6_internal instead. The Bicep layer doesn’t really know how to use those feature flags directly.

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


log.info(f"vm setting: {azure_node_runbook}")

if is_windows:
Expand Down
25 changes: 18 additions & 7 deletions 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 @@ -340,11 +340,19 @@ def setup_tap(self, name: str, bridge: str) -> None:
# start interface
self.up(name)

def get_ip_address(self, nic_name: str) -> str:
result = self.run(f"addr show {nic_name}", force_run=True, sudo=True)
matched = self._get_matched_dict(result.stdout)
assert "ip_addr" in matched, f"not find ip address for nic {nic_name}"
return matched["ip_addr"]
def get_ip_address(self, nic_name: str, ipv6: bool = False) -> str:
if ipv6:
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
else:
result = self.run(f"addr show {nic_name}", force_run=True, sudo=True)
matched = self._get_matched_dict(result.stdout)
assert "ip_addr" in matched, f"not find ip address for nic {nic_name}"
return matched["ip_addr"]

def get_default_route_info(self) -> tuple[str, str]:
result = self.run("route", force_run=True, sudo=True)
Expand Down Expand Up @@ -462,7 +470,10 @@ def get_mac(self, nic_name: str) -> str:
assert_that(matched).described_as("could not find mac address").is_length(1)
return str(matched[0][0])

def get_ip_address(self, nic_name: str) -> str:
def get_ip_address(self, nic_name: str, ipv6: bool = False) -> str:
if ipv6:
raise NotImplementedError("IPv6 support not implemented for FreeBSD")

output = self.run(
nic_name,
force_run=True,
Expand Down
Loading
Loading