From 5dba26c86ee057ac5da76343884059d275bbe779 Mon Sep 17 00:00:00 2001 From: cdossa Date: Tue, 28 Apr 2026 10:25:49 -0700 Subject: [PATCH 1/6] [aks-preview] Add --node-public-ip-prefix-ids for dual-stack ILPIP support Add a new CLI parameter --node-public-ip-prefix-ids (comma-separated list of public IP prefix resource IDs) to 'az aks create' and 'az aks nodepool add' for dual-stack instance-level public IPs (IPv4 and/or IPv6). - Register parameter in _params.py for aks create and nodepool add - Add mutual exclusion validator with --node-public-ip-prefix-id (singular) - Wire parameter through custom.py function signatures - Add get_node_public_ip_prefix_ids() context method in agentpool_decorator - Set network_profile.node_public_ip_prefix_ids on the SDK model - Auto-enable enable_node_public_ip when prefix IDs are provided - Add node_public_ip_prefix_ids field to vendored AgentPoolNetworkProfile - Add unit tests for the new context method - Not added to nodepool update (field is immutable on existing pools) --- src/aks-preview/azext_aks_preview/_params.py | 17 +++++++++ .../azext_aks_preview/_validators.py | 10 ++++++ .../azext_aks_preview/agentpool_decorator.py | 19 ++++++++++ src/aks-preview/azext_aks_preview/custom.py | 2 ++ .../tests/latest/test_agentpool_decorator.py | 36 +++++++++++++++++++ .../latest/test_managed_cluster_decorator.py | 1 + .../azure_mgmt_preview_aks/models/_models.py | 9 +++++ 7 files changed, 94 insertions(+) diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index b78493e8286..11d2613fdca 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -211,6 +211,7 @@ validate_load_balancer_sku, validate_max_surge, validate_message_of_the_day, + validate_node_public_ip_prefix_ids, validate_node_public_ip_tags, validate_nodepool_id, validate_nodepool_labels, @@ -868,6 +869,14 @@ def load_arguments(self, _): ) c.argument("enable_node_public_ip", action="store_true") c.argument("node_public_ip_prefix_id") + c.argument( + "node_public_ip_prefix_ids", + validator=validate_node_public_ip_prefix_ids, + help="Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs " + "(IPv4 and/or IPv6). At most one IPv4 and one IPv6 prefix may be specified. " + "Requires --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " + "Requires the NodePublicIPv6PrefixPreview feature flag to be registered.", + ) c.argument("enable_cluster_autoscaler", action="store_true") c.argument("min_count", type=int, validator=validate_nodes_count) c.argument("max_count", type=int, validator=validate_nodes_count) @@ -2042,6 +2051,14 @@ def load_arguments(self, _): ) c.argument("enable_node_public_ip", action="store_true") c.argument("node_public_ip_prefix_id") + c.argument( + "node_public_ip_prefix_ids", + validator=validate_node_public_ip_prefix_ids, + help="Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs " + "(IPv4 and/or IPv6). At most one IPv4 and one IPv6 prefix may be specified. " + "Requires --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " + "Requires the NodePublicIPv6PrefixPreview feature flag to be registered.", + ) c.argument( "enable_cluster_autoscaler", options_list=["--enable-cluster-autoscaler", "-e"], diff --git a/src/aks-preview/azext_aks_preview/_validators.py b/src/aks-preview/azext_aks_preview/_validators.py index aa43b9052a2..abbfa81add2 100644 --- a/src/aks-preview/azext_aks_preview/_validators.py +++ b/src/aks-preview/azext_aks_preview/_validators.py @@ -393,6 +393,16 @@ def validate_node_public_ip_tags(ns): ns.node_public_ip_tags = tags_dict +def validate_node_public_ip_prefix_ids(ns): + """Validate --node-public-ip-prefix-ids is not used together with --node-public-ip-prefix-id.""" + ids_value = getattr(ns, "node_public_ip_prefix_ids", None) + singular_value = getattr(ns, "node_public_ip_prefix_id", None) + if ids_value and singular_value: + raise MutuallyExclusiveArgumentError( + "--node-public-ip-prefix-ids and --node-public-ip-prefix-id cannot be used at the same time." + ) + + def validate_nodepool_labels(namespace): """Validates that provided node labels is a valid format""" diff --git a/src/aks-preview/azext_aks_preview/agentpool_decorator.py b/src/aks-preview/azext_aks_preview/agentpool_decorator.py index a61272f076b..077afe971db 100644 --- a/src/aks-preview/azext_aks_preview/agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/agentpool_decorator.py @@ -338,6 +338,20 @@ def get_ip_tags(self) -> Union[List[IPTag], None]: )) return res + def get_node_public_ip_prefix_ids(self) -> Union[List[str], None]: + """Obtain the value of node_public_ip_prefix_ids. + + Parse the comma-separated string into a list of resource IDs. + + :return: list of strings or None + """ + node_public_ip_prefix_ids = self.raw_param.get("node_public_ip_prefix_ids") + if node_public_ip_prefix_ids: + if isinstance(node_public_ip_prefix_ids, str): + return [x.strip() for x in node_public_ip_prefix_ids.split(",") if x.strip()] + return node_public_ip_prefix_ids + return None + def get_node_taints(self) -> Union[List[str], None]: """Obtain the value of node_taints. @@ -1306,6 +1320,11 @@ def set_up_agentpool_network_profile(self, agentpool: AgentPool) -> AgentPool: if ip_tags: agentpool.network_profile.node_public_ip_tags = ip_tags + node_public_ip_prefix_ids = self.context.get_node_public_ip_prefix_ids() + if node_public_ip_prefix_ids: + agentpool.network_profile.node_public_ip_prefix_ids = node_public_ip_prefix_ids + agentpool.enable_node_public_ip = True + return agentpool def set_up_taints(self, agentpool: AgentPool) -> AgentPool: diff --git a/src/aks-preview/azext_aks_preview/custom.py b/src/aks-preview/azext_aks_preview/custom.py index 59422a1fb1c..ead47015e22 100644 --- a/src/aks-preview/azext_aks_preview/custom.py +++ b/src/aks-preview/azext_aks_preview/custom.py @@ -1054,6 +1054,7 @@ def aks_create( pod_ip_allocation_mode=None, enable_node_public_ip=False, node_public_ip_prefix_id=None, + node_public_ip_prefix_ids=None, enable_cluster_autoscaler=False, min_count=None, max_count=None, @@ -1908,6 +1909,7 @@ def aks_agentpool_add( pod_ip_allocation_mode=None, enable_node_public_ip=False, node_public_ip_prefix_id=None, + node_public_ip_prefix_ids=None, enable_cluster_autoscaler=False, min_count=None, max_count=None, diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py index e51e73c6c20..73488138273 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py @@ -346,6 +346,36 @@ def common_get_pod_ip_allocation_mode(self): ctx_2.attach_agentpool(agentpool_2) self.assertEqual(ctx_2.get_pod_ip_allocation_mode(), "StaticBlock") + def common_get_node_public_ip_prefix_ids(self): + # default - None + ctx_1 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"node_public_ip_prefix_ids": None}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual(ctx_1.get_node_public_ip_prefix_ids(), None) + + # comma-separated string + ctx_2 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({ + "node_public_ip_prefix_ids": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv4-prefix," + "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv6-prefix" + }), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + self.assertEqual( + ctx_2.get_node_public_ip_prefix_ids(), + [ + "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv4-prefix", + "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPPrefixes/ipv6-prefix", + ], + ) + def common_get_skip_gpu_driver_install(self): # default ctx_1 = AKSPreviewAgentPoolContext( @@ -1164,6 +1194,9 @@ def test_get_enable_managed_gpu(self): def test_get_pod_ip_allocation_mode(self): self.common_get_pod_ip_allocation_mode() + def test_get_node_public_ip_prefix_ids(self): + self.common_get_node_public_ip_prefix_ids() + def test_get_os_sku(self): self.common_get_os_sku() @@ -1264,6 +1297,9 @@ def test_get_enable_managed_gpu(self): def test_get_pod_ip_allocation_mode(self): self.common_get_pod_ip_allocation_mode() + def test_get_node_public_ip_prefix_ids(self): + self.common_get_node_public_ip_prefix_ids() + def test_get_os_sku(self): self.common_get_os_sku() diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py index 5e94ae51e06..81307edec15 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_managed_cluster_decorator.py @@ -5959,6 +5959,7 @@ def test_set_up_agentpool_profile(self): "pod_ip_allocation_mode": "DynamicIndividual", "enable_node_public_ip": True, "node_public_ip_prefix_id": "test_node_public_ip_prefix_id", + "node_public_ip_prefix_ids": None, "enable_cluster_autoscaler": True, "min_count": 5, "max_count": 20, diff --git a/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py b/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py index 329b1ac1ade..e7e9651dde2 100644 --- a/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py +++ b/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py @@ -1422,6 +1422,9 @@ class AgentPoolNetworkProfile(_Model): :ivar application_security_groups: The IDs of the application security groups which agent pool will associate when created. :vartype application_security_groups: list[str] + :ivar node_public_ip_prefix_ids: The public IP prefix IDs which VM nodes should use IPs from. + At most one IPv4 and one IPv6 prefix may be specified for dual-stack node public IPs. + :vartype node_public_ip_prefix_ids: list[str] """ node_public_ip_tags: Optional[list["_models.IPTag"]] = rest_field( @@ -1436,6 +1439,11 @@ class AgentPoolNetworkProfile(_Model): name="applicationSecurityGroups", visibility=["read", "create", "update", "delete", "query"] ) """The IDs of the application security groups which agent pool will associate when created.""" + node_public_ip_prefix_ids: Optional[list[str]] = rest_field( + name="nodePublicIPPrefixIDs", visibility=["read", "create", "update", "delete", "query"] + ) + """The public IP prefix IDs which VM nodes should use IPs from. At most one IPv4 and one IPv6 + prefix may be specified for dual-stack node public IPs.""" @overload def __init__( @@ -1444,6 +1452,7 @@ def __init__( node_public_ip_tags: Optional[list["_models.IPTag"]] = None, allowed_host_ports: Optional[list["_models.PortRange"]] = None, application_security_groups: Optional[list[str]] = None, + node_public_ip_prefix_ids: Optional[list[str]] = None, ) -> None: ... @overload From 896db12ba45fbf2bbb122bdb8296dede99c4df95 Mon Sep 17 00:00:00 2001 From: cdossa Date: Tue, 28 Apr 2026 11:17:33 -0700 Subject: [PATCH 2/6] Add linter exclusions for --node-public-ip-prefix-ids option length --- linter_exclusions.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/linter_exclusions.yml b/linter_exclusions.yml index 9a603f25ac7..3ddcd7800ed 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -37,6 +37,9 @@ aks create: node_public_ip_prefix_id: rule_exclusions: - option_length_too_long + node_public_ip_prefix_ids: + rule_exclusions: + - option_length_too_long enable_private_cluster: rule_exclusions: - option_length_too_long @@ -130,6 +133,11 @@ aks create: service_account_image_pull_default_managed_identity_id: rule_exclusions: - option_length_too_long +aks nodepool add: + parameters: + node_public_ip_prefix_ids: + rule_exclusions: + - option_length_too_long aks update: parameters: aad_admin_group_object_ids: From 32f8aecc2adebda7ee83c84555af266a7077fb56 Mon Sep 17 00:00:00 2001 From: cdossa Date: Tue, 28 Apr 2026 13:11:20 -0700 Subject: [PATCH 3/6] Bump version to 20.0.0b5 and add HISTORY.rst entry --- src/aks-preview/HISTORY.rst | 5 +++++ src/aks-preview/setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/aks-preview/HISTORY.rst b/src/aks-preview/HISTORY.rst index 792ef11e661..44ffe3eeb36 100644 --- a/src/aks-preview/HISTORY.rst +++ b/src/aks-preview/HISTORY.rst @@ -13,6 +13,11 @@ Pending +++++++ * `az aks create`: Add `--control-plane-scaling-size` parameter to configure control plane scaling profile with available sizes 'H2', 'H4', and 'H8'. +20.0.0b5 ++++++++ +* `az aks create`: Add `--node-public-ip-prefix-ids` parameter for specifying dual-stack (IPv4/IPv6) public IP prefixes for instance-level public IPs. +* `az aks nodepool add`: Add `--node-public-ip-prefix-ids` parameter for specifying dual-stack (IPv4/IPv6) public IP prefixes for instance-level public IPs. + 20.0.0b4 +++++++ * `az aks nodepool update`: Support `--node-vm-size` to resize VM size of an existing VMSS-based agent pool (preview). Requires AFEC registration `Microsoft.ContainerService/AgentPoolVMSSResize`. diff --git a/src/aks-preview/setup.py b/src/aks-preview/setup.py index 44030af6dc3..785e8a41d65 100644 --- a/src/aks-preview/setup.py +++ b/src/aks-preview/setup.py @@ -9,7 +9,7 @@ from setuptools import find_packages, setup -VERSION = "20.0.0b4" +VERSION = "20.0.0b5" CLASSIFIERS = [ "Development Status :: 4 - Beta", From a51d88289e61402baab203d3d038f94c732c7119 Mon Sep 17 00:00:00 2001 From: cdossa Date: Tue, 28 Apr 2026 14:19:27 -0700 Subject: [PATCH 4/6] Address Copilot review: fix help text, enhance validation, add _help.py entries - Fix help text: 'Requires --enable-node-public-ip' -> 'Automatically enables' - Add --node-public-ip-prefix-ids to _help.py for both aks create and nodepool add - Enhance validator: check non-empty, max 2 IDs, valid resource IDs - Handle empty string in decorator: raise InvalidArgumentValueError - Add unit test for empty string error case --- src/aks-preview/azext_aks_preview/_help.py | 8 ++++++++ src/aks-preview/azext_aks_preview/_params.py | 4 ++-- .../azext_aks_preview/_validators.py | 18 +++++++++++++++++- .../azext_aks_preview/agentpool_decorator.py | 19 ++++++++++++++----- .../tests/latest/test_agentpool_decorator.py | 11 +++++++++++ 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/_help.py b/src/aks-preview/azext_aks_preview/_help.py index 300f74b5791..bbec4bb6c7b 100644 --- a/src/aks-preview/azext_aks_preview/_help.py +++ b/src/aks-preview/azext_aks_preview/_help.py @@ -358,6 +358,10 @@ - name: --node-public-ip-prefix-id type: string short-summary: Public IP prefix ID used to assign public IPs to VMSS nodes. + - name: --node-public-ip-prefix-ids + type: string + short-summary: Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs (IPv4 and/or IPv6). + long-summary: At most one IPv4 and one IPv6 prefix may be specified. Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. Requires the NodePublicIPv6PrefixPreview feature flag. - name: --enable-managed-identity type: bool short-summary: Using managed identity to manage cluster resource group. You can explicitly specify "--service-principal" and "--client-secret" to disable managed identity, otherwise it will be enabled. @@ -2155,6 +2159,10 @@ - name: --node-public-ip-prefix-id type: string short-summary: Public IP prefix ID used to assign public IPs to VMSS nodes. Must use VMSS agent pool type. + - name: --node-public-ip-prefix-ids + type: string + short-summary: Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs (IPv4 and/or IPv6). + long-summary: At most one IPv4 and one IPv6 prefix may be specified. Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. Requires the NodePublicIPv6PrefixPreview feature flag. - name: --labels type: string short-summary: The node labels for the node pool. See https://aka.ms/node-labels for syntax of labels. diff --git a/src/aks-preview/azext_aks_preview/_params.py b/src/aks-preview/azext_aks_preview/_params.py index 11d2613fdca..d009c4b7dd8 100644 --- a/src/aks-preview/azext_aks_preview/_params.py +++ b/src/aks-preview/azext_aks_preview/_params.py @@ -874,7 +874,7 @@ def load_arguments(self, _): validator=validate_node_public_ip_prefix_ids, help="Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs " "(IPv4 and/or IPv6). At most one IPv4 and one IPv6 prefix may be specified. " - "Requires --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " + "Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " "Requires the NodePublicIPv6PrefixPreview feature flag to be registered.", ) c.argument("enable_cluster_autoscaler", action="store_true") @@ -2056,7 +2056,7 @@ def load_arguments(self, _): validator=validate_node_public_ip_prefix_ids, help="Comma-separated list of public IP prefix resource IDs for dual-stack node public IPs " "(IPv4 and/or IPv6). At most one IPv4 and one IPv6 prefix may be specified. " - "Requires --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " + "Automatically enables --enable-node-public-ip. Cannot be used with --node-public-ip-prefix-id. " "Requires the NodePublicIPv6PrefixPreview feature flag to be registered.", ) c.argument( diff --git a/src/aks-preview/azext_aks_preview/_validators.py b/src/aks-preview/azext_aks_preview/_validators.py index abbfa81add2..d359952cf13 100644 --- a/src/aks-preview/azext_aks_preview/_validators.py +++ b/src/aks-preview/azext_aks_preview/_validators.py @@ -394,13 +394,29 @@ def validate_node_public_ip_tags(ns): def validate_node_public_ip_prefix_ids(ns): - """Validate --node-public-ip-prefix-ids is not used together with --node-public-ip-prefix-id.""" + """Validate --node-public-ip-prefix-ids value and mutual exclusion with --node-public-ip-prefix-id.""" ids_value = getattr(ns, "node_public_ip_prefix_ids", None) singular_value = getattr(ns, "node_public_ip_prefix_id", None) if ids_value and singular_value: raise MutuallyExclusiveArgumentError( "--node-public-ip-prefix-ids and --node-public-ip-prefix-id cannot be used at the same time." ) + if ids_value is not None: + parsed = [x.strip() for x in ids_value.split(",") if x.strip()] if isinstance(ids_value, str) else ids_value + if not parsed: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids must contain at least one public IP prefix resource ID." + ) + if len(parsed) > 2: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids accepts at most two public IP prefix resource IDs " + "(one IPv4 and one IPv6)." + ) + for prefix_id in parsed: + if not is_valid_resource_id(prefix_id): + raise InvalidArgumentValueError( + f"'{prefix_id}' is not a valid Azure resource ID for --node-public-ip-prefix-ids." + ) def validate_nodepool_labels(namespace): diff --git a/src/aks-preview/azext_aks_preview/agentpool_decorator.py b/src/aks-preview/azext_aks_preview/agentpool_decorator.py index 077afe971db..4feaae2ce3d 100644 --- a/src/aks-preview/azext_aks_preview/agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/agentpool_decorator.py @@ -346,11 +346,20 @@ def get_node_public_ip_prefix_ids(self) -> Union[List[str], None]: :return: list of strings or None """ node_public_ip_prefix_ids = self.raw_param.get("node_public_ip_prefix_ids") - if node_public_ip_prefix_ids: - if isinstance(node_public_ip_prefix_ids, str): - return [x.strip() for x in node_public_ip_prefix_ids.split(",") if x.strip()] - return node_public_ip_prefix_ids - return None + if node_public_ip_prefix_ids is None: + return None + if isinstance(node_public_ip_prefix_ids, str): + parsed = [x.strip() for x in node_public_ip_prefix_ids.split(",") if x.strip()] + if not parsed: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids must contain at least one public IP prefix resource ID." + ) + return parsed + if isinstance(node_public_ip_prefix_ids, list) and not node_public_ip_prefix_ids: + raise InvalidArgumentValueError( + "--node-public-ip-prefix-ids must contain at least one public IP prefix resource ID." + ) + return node_public_ip_prefix_ids def get_node_taints(self) -> Union[List[str], None]: """Obtain the value of node_taints. diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py index 73488138273..441e7869ec3 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py @@ -376,6 +376,17 @@ def common_get_node_public_ip_prefix_ids(self): ], ) + # empty string should raise error + ctx_3 = AKSPreviewAgentPoolContext( + self.cmd, + AKSAgentPoolParamDict({"node_public_ip_prefix_ids": ""}), + self.models, + DecoratorMode.CREATE, + self.agentpool_decorator_mode, + ) + with self.assertRaises(InvalidArgumentValueError): + ctx_3.get_node_public_ip_prefix_ids() + def common_get_skip_gpu_driver_install(self): # default ctx_1 = AKSPreviewAgentPoolContext( From 9f6a330a02696614420271643c34e2532b05ade3 Mon Sep 17 00:00:00 2001 From: cdossa Date: Wed, 29 Apr 2026 11:10:19 -0700 Subject: [PATCH 5/6] Address Fuming's review: revert manual SDK change, add scenario test - Revert manual edits to vendored SDK _models.py (AgentPoolNetworkProfile) - Use dictionary-style access (network_profile["nodePublicIPPrefixIDs"]) instead of attribute access, so the field is serialized correctly without requiring the SDK to be re-vendored - Add scenario test test_node_public_ip_prefix_ids covering both aks create and aks nodepool add with dual-stack IP prefixes --- .../azext_aks_preview/agentpool_decorator.py | 2 +- .../tests/latest/test_aks_commands.py | 113 ++++++++++++++++++ .../azure_mgmt_preview_aks/models/_models.py | 9 -- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/aks-preview/azext_aks_preview/agentpool_decorator.py b/src/aks-preview/azext_aks_preview/agentpool_decorator.py index 4feaae2ce3d..80c5c2d1eac 100644 --- a/src/aks-preview/azext_aks_preview/agentpool_decorator.py +++ b/src/aks-preview/azext_aks_preview/agentpool_decorator.py @@ -1331,7 +1331,7 @@ def set_up_agentpool_network_profile(self, agentpool: AgentPool) -> AgentPool: node_public_ip_prefix_ids = self.context.get_node_public_ip_prefix_ids() if node_public_ip_prefix_ids: - agentpool.network_profile.node_public_ip_prefix_ids = node_public_ip_prefix_ids + agentpool.network_profile["nodePublicIPPrefixIDs"] = node_public_ip_prefix_ids agentpool.enable_node_public_ip = True return agentpool diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index bc3870af232..61e3d65ccd3 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -17293,6 +17293,119 @@ def test_node_public_ip_tags(self, resource_group, resource_group_location): ], ) + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer( + random_name_length=17, name_prefix="clitest", location="eastus2euap" + ) + def test_node_public_ip_prefix_ids(self, resource_group, resource_group_location): + aks_name = self.create_random_name("cliakstest", 16) + nodepool_name = self.create_random_name("n", 6) + nodepool_name_1 = self.create_random_name("n", 6) + + self.kwargs.update( + { + "resource_group": resource_group, + "name": aks_name, + "location": resource_group_location, + "ssh_key_value": self.generate_ssh_keys(), + "node_pool_name": nodepool_name, + "node_vm_size": "standard_d2a_v4", + "ipv4_prefix": self.create_random_name("ipv4prefix", 20), + "ipv6_prefix": self.create_random_name("ipv6prefix", 20), + } + ) + + # Create IPv4 and IPv6 public IP prefixes + ipv4_result = self.cmd( + "network public-ip prefix create " + "--resource-group={resource_group} " + "--name={ipv4_prefix} " + "--location={location} " + "--length=28 " + "--version=IPv4" + ).get_output_in_json() + ipv4_prefix_id = ipv4_result["id"] + + ipv6_result = self.cmd( + "network public-ip prefix create " + "--resource-group={resource_group} " + "--name={ipv6_prefix} " + "--location={location} " + "--length=124 " + "--version=IPv6" + ).get_output_in_json() + ipv6_prefix_id = ipv6_result["id"] + + self.kwargs.update( + { + "prefix_ids": f"{ipv4_prefix_id},{ipv6_prefix_id}", + } + ) + + # Create cluster with --node-public-ip-prefix-ids + self.cmd( + "aks create " + "--resource-group={resource_group} " + "--name={name} " + "--location={location} " + "--ssh-key-value={ssh_key_value} " + "--nodepool-name={node_pool_name} " + "--node-count=1 " + "--node-vm-size={node_vm_size} " + "--node-public-ip-prefix-ids={prefix_ids} " + "--aks-custom-headers=AKSHTTPCustomFeatures=Microsoft.ContainerService/NodePublicIPv6PrefixPreview", + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("agentPoolProfiles[0].enableNodePublicIp", True), + self.check( + "agentPoolProfiles[0].networkProfile.nodePublicIPPrefixIDs[0]", + ipv4_prefix_id, + ), + self.check( + "agentPoolProfiles[0].networkProfile.nodePublicIPPrefixIDs[1]", + ipv6_prefix_id, + ), + ], + ) + + # Add nodepool with --node-public-ip-prefix-ids + self.kwargs.update( + { + "node_pool_name": nodepool_name_1, + } + ) + + self.cmd( + "aks nodepool add " + "--resource-group={resource_group} " + "--cluster-name={name} " + "--name={node_pool_name} " + "--node-count=1 " + "--node-vm-size={node_vm_size} " + "--node-public-ip-prefix-ids={prefix_ids} " + "--aks-custom-headers=AKSHTTPCustomFeatures=Microsoft.ContainerService/NodePublicIPv6PrefixPreview", + checks=[ + self.check("provisioningState", "Succeeded"), + self.check("enableNodePublicIp", True), + self.check( + "networkProfile.nodePublicIPPrefixIDs[0]", + ipv4_prefix_id, + ), + self.check( + "networkProfile.nodePublicIPPrefixIDs[1]", + ipv6_prefix_id, + ), + ], + ) + + # delete + self.cmd( + "aks delete --resource-group={resource_group} --name={name} --yes --no-wait", + checks=[ + self.is_empty(), + ], + ) + @AllowLargeResponse() @AKSCustomResourceGroupPreparer( random_name_length=17, name_prefix="clitest", location="westus2" diff --git a/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py b/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py index e7e9651dde2..329b1ac1ade 100644 --- a/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py +++ b/src/aks-preview/azext_aks_preview/vendored_sdks/azure_mgmt_preview_aks/models/_models.py @@ -1422,9 +1422,6 @@ class AgentPoolNetworkProfile(_Model): :ivar application_security_groups: The IDs of the application security groups which agent pool will associate when created. :vartype application_security_groups: list[str] - :ivar node_public_ip_prefix_ids: The public IP prefix IDs which VM nodes should use IPs from. - At most one IPv4 and one IPv6 prefix may be specified for dual-stack node public IPs. - :vartype node_public_ip_prefix_ids: list[str] """ node_public_ip_tags: Optional[list["_models.IPTag"]] = rest_field( @@ -1439,11 +1436,6 @@ class AgentPoolNetworkProfile(_Model): name="applicationSecurityGroups", visibility=["read", "create", "update", "delete", "query"] ) """The IDs of the application security groups which agent pool will associate when created.""" - node_public_ip_prefix_ids: Optional[list[str]] = rest_field( - name="nodePublicIPPrefixIDs", visibility=["read", "create", "update", "delete", "query"] - ) - """The public IP prefix IDs which VM nodes should use IPs from. At most one IPv4 and one IPv6 - prefix may be specified for dual-stack node public IPs.""" @overload def __init__( @@ -1452,7 +1444,6 @@ def __init__( node_public_ip_tags: Optional[list["_models.IPTag"]] = None, allowed_host_ports: Optional[list["_models.PortRange"]] = None, application_security_groups: Optional[list[str]] = None, - node_public_ip_prefix_ids: Optional[list[str]] = None, ) -> None: ... @overload From b07ca044cca21d46181be7e491544cc5baeb5cf3 Mon Sep 17 00:00:00 2001 From: cdossa Date: Wed, 29 Apr 2026 13:08:33 -0700 Subject: [PATCH 6/6] Mark test_node_public_ip_prefix_ids as live_only The scenario test requires the NodePublicIPv6PrefixPreview feature flag and Azure credentials. Mark it @live_only() so it is skipped during CI recording-based test runs. --- .../azext_aks_preview/tests/latest/test_aks_commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py index 61e3d65ccd3..c9d818a2f54 100644 --- a/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py +++ b/src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py @@ -17293,6 +17293,9 @@ def test_node_public_ip_tags(self, resource_group, resource_group_location): ], ) + # live_only: NodePublicIPv6PrefixPreview feature flag must be registered on the + # test subscription. Recording-based tests will be added once the feature is GA. + @live_only() @AllowLargeResponse() @AKSCustomResourceGroupPreparer( random_name_length=17, name_prefix="clitest", location="eastus2euap"