Skip to content

Commit 3153613

Browse files
{AKS} Add support for provisioning secondary network interfaces in node pool (#9886)
1 parent 42894d6 commit 3153613

7 files changed

Lines changed: 267 additions & 0 deletions

File tree

src/aks-preview/HISTORY.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ To release a new version, please select a new version number (usually plus 1 to
1111

1212
Pending
1313
+++++++
14+
* `az aks nodepool add`: Add `--secondary-network-interfaces`/`--secondary-nics` (preview) to configure secondary network interfaces on agent pool nodes. Accepts inline JSON or `@file`. Property is immutable after node pool creation.
1415
* `az aks update`: Add `--control-plane-scaling-size` parameter to update the control plane scaling size on an existing cluster with available sizes 'H2', 'H4', and 'H8'.
1516
* `az aks bastion`: Fix `--subscription` not being passed to internal `az network bastion tunnel` and bastion discovery commands.
1617
* `az aks update`: Add `--node-disruption-policy` (preview) to update the node disruption policy for a cluster. Requires AFEC registration `Microsoft.ContainerService/NodeDisruptionProfile`. This is a cluster-level property that applies to all node pools in the cluster.

src/aks-preview/azext_aks_preview/_help.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,6 +2336,13 @@
23362336
- name: --localdns-config
23372337
type: string
23382338
short-summary: Set the localDNS Profile for a nodepool with a JSON config file.
2339+
- name: --secondary-network-interfaces --secondary-nics
2340+
type: string
2341+
short-summary: Secondary network interface configurations as a JSON string or `@filename`.
2342+
long-summary: |-
2343+
Specify secondary NICs to attach to each node. Accepts inline JSON or `@filename`.
2344+
Example: '[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet","enableAcceleratedNetworking":true}]'
2345+
Supported NIC types are "Standard" (requires vnetSubnetId) and "Dynamic".
23392346
- name: --upgrade-strategy
23402347
type: string
23412348
short-summary: Upgrade strategy for the node pool. Allowed values are "Rolling" or "BlueGreen". Default is "Rolling".

src/aks-preview/azext_aks_preview/_params.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2252,6 +2252,14 @@ def load_arguments(self, _):
22522252
'localdns_config',
22532253
help='Path to a JSON file to configure the local DNS profile for a new nodepool.'
22542254
)
2255+
# secondary network interfaces
2256+
c.argument(
2257+
'secondary_network_interfaces',
2258+
options_list=['--secondary-network-interfaces', '--secondary-nics'],
2259+
help='Secondary network interface configurations as a JSON string or `@filename` to load from a file. '
2260+
'Example: \'[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet"}]\'',
2261+
is_preview=True,
2262+
)
22552263

22562264
with self.argument_context("aks nodepool update") as c:
22572265
c.argument(

src/aks-preview/azext_aks_preview/agentpool_decorator.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from azure.cli.core.util import (
2929
read_file_content,
3030
sdk_no_wait,
31+
shell_safe_json_parse,
3132
)
3233
from azure.core import MatchConditions
3334
from knack.log import get_logger
@@ -984,6 +985,39 @@ def get_localdns_profile(self):
984985
return profile
985986
return None
986987

988+
def get_secondary_network_interfaces(self):
989+
"""Obtain the value of secondary_network_interfaces.
990+
991+
Parse inline JSON or @file reference into a list of AgentPoolNetworkInterface models.
992+
"""
993+
raw = self.raw_param.get("secondary_network_interfaces")
994+
if raw is None:
995+
return None
996+
if isinstance(raw, str):
997+
if raw.startswith("@"):
998+
data = get_file_json(raw[1:])
999+
else:
1000+
data = shell_safe_json_parse(raw)
1001+
else:
1002+
data = raw
1003+
if not isinstance(data, list):
1004+
raise InvalidArgumentValueError(
1005+
"--secondary-network-interfaces must be a JSON array."
1006+
)
1007+
result = []
1008+
for idx, item in enumerate(data):
1009+
if not isinstance(item, dict):
1010+
raise InvalidArgumentValueError(
1011+
f"--secondary-network-interfaces: element at index {idx} "
1012+
f"must be a JSON object, got {type(item).__name__}."
1013+
)
1014+
result.append(self.models.AgentPoolNetworkInterface(
1015+
type=item.get("type"),
1016+
vnet_subnet_id=item.get("vnetSubnetId"),
1017+
enable_accelerated_networking=item.get("enableAcceleratedNetworking"),
1018+
))
1019+
return result
1020+
9871021
def build_localdns_profile(self, agentpool: AgentPool) -> AgentPool:
9881022
"""Build local DNS profile for the AgentPool object if provided via --localdns-config."""
9891023
localdns_profile = self.get_localdns_profile()
@@ -1332,6 +1366,10 @@ def set_up_agentpool_network_profile(self, agentpool: AgentPool) -> AgentPool:
13321366
agentpool.network_profile.node_public_ip_prefix_i_ds = node_public_ip_prefix_ids
13331367
agentpool.enable_node_public_ip = True
13341368

1369+
secondary_nics = self.context.get_secondary_network_interfaces()
1370+
if secondary_nics is not None:
1371+
agentpool.network_profile.secondary_network_interfaces = secondary_nics
1372+
13351373
return agentpool
13361374

13371375
def set_up_taints(self, agentpool: AgentPool) -> AgentPool:

src/aks-preview/azext_aks_preview/custom.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1995,6 +1995,8 @@ def aks_agentpool_add(
19951995
vm_sizes=None,
19961996
# local DNS
19971997
localdns_config=None,
1998+
# secondary network interfaces
1999+
secondary_network_interfaces=None,
19982000
):
19992001
# DO NOT MOVE: get all the original parameters and save them as a dictionary
20002002
raw_parameters = locals()

src/aks-preview/azext_aks_preview/tests/latest/test_agentpool_decorator.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,86 @@ def common_get_final_soak_duration(self):
12181218
ctx_3.attach_agentpool(agentpool_3)
12191219
self.assertEqual(ctx_3.get_final_soak_duration(), 1200)
12201220

1221+
def common_get_secondary_network_interfaces(self):
1222+
# default - None
1223+
ctx_1 = AKSPreviewAgentPoolContext(
1224+
self.cmd,
1225+
AKSAgentPoolParamDict({"secondary_network_interfaces": None}),
1226+
self.models,
1227+
DecoratorMode.CREATE,
1228+
self.agentpool_decorator_mode,
1229+
)
1230+
self.assertEqual(ctx_1.get_secondary_network_interfaces(), None)
1231+
1232+
# inline JSON
1233+
ctx_2 = AKSPreviewAgentPoolContext(
1234+
self.cmd,
1235+
AKSAgentPoolParamDict({
1236+
"secondary_network_interfaces": '[{"type":"Standard","vnetSubnetId":"/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1"}]'
1237+
}),
1238+
self.models,
1239+
DecoratorMode.CREATE,
1240+
self.agentpool_decorator_mode,
1241+
)
1242+
result = ctx_2.get_secondary_network_interfaces()
1243+
self.assertIsNotNone(result)
1244+
self.assertEqual(len(result), 1)
1245+
self.assertEqual(result[0].type, "Standard")
1246+
self.assertEqual(result[0].vnet_subnet_id, "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1")
1247+
1248+
# invalid JSON - not a list
1249+
ctx_3 = AKSPreviewAgentPoolContext(
1250+
self.cmd,
1251+
AKSAgentPoolParamDict({
1252+
"secondary_network_interfaces": '{"type":"Standard"}'
1253+
}),
1254+
self.models,
1255+
DecoratorMode.CREATE,
1256+
self.agentpool_decorator_mode,
1257+
)
1258+
with self.assertRaises(InvalidArgumentValueError):
1259+
ctx_3.get_secondary_network_interfaces()
1260+
1261+
# invalid JSON - array element is not a dict
1262+
ctx_4 = AKSPreviewAgentPoolContext(
1263+
self.cmd,
1264+
AKSAgentPoolParamDict({
1265+
"secondary_network_interfaces": '[null]'
1266+
}),
1267+
self.models,
1268+
DecoratorMode.CREATE,
1269+
self.agentpool_decorator_mode,
1270+
)
1271+
with self.assertRaises(InvalidArgumentValueError):
1272+
ctx_4.get_secondary_network_interfaces()
1273+
1274+
# @file input
1275+
import tempfile
1276+
import json
1277+
nics_data = [{"type": "Dynamic"}, {"type": "Standard", "vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1"}]
1278+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
1279+
json.dump(nics_data, f)
1280+
tmp_path = f.name
1281+
try:
1282+
ctx_5 = AKSPreviewAgentPoolContext(
1283+
self.cmd,
1284+
AKSAgentPoolParamDict({
1285+
"secondary_network_interfaces": f"@{tmp_path}"
1286+
}),
1287+
self.models,
1288+
DecoratorMode.CREATE,
1289+
self.agentpool_decorator_mode,
1290+
)
1291+
result = ctx_5.get_secondary_network_interfaces()
1292+
self.assertEqual(len(result), 2)
1293+
self.assertEqual(result[0].type, "Dynamic")
1294+
self.assertIsNone(result[0].vnet_subnet_id)
1295+
self.assertEqual(result[1].type, "Standard")
1296+
self.assertEqual(result[1].vnet_subnet_id, "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1")
1297+
finally:
1298+
import os
1299+
os.unlink(tmp_path)
1300+
12211301

12221302
class AKSPreviewAgentPoolContextStandaloneModeTestCase(
12231303
AKSPreviewAgentPoolContextCommonTestCase
@@ -1326,6 +1406,9 @@ def test_get_batch_soak_duration(self):
13261406
def test_get_final_soak_duration(self):
13271407
self.common_get_final_soak_duration()
13281408

1409+
def test_get_secondary_network_interfaces(self):
1410+
self.common_get_secondary_network_interfaces()
1411+
13291412

13301413
class AKSPreviewAgentPoolContextManagedClusterModeTestCase(
13311414
AKSPreviewAgentPoolContextCommonTestCase

src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16926,6 +16926,134 @@ def test_aks_nodepool_create_with_nsg_control(
1692616926
],
1692716927
)
1692816928

16929+
@live_only()
16930+
@AllowLargeResponse()
16931+
@AKSCustomResourceGroupPreparer(
16932+
random_name_length=17, name_prefix="clitest", location="westus2"
16933+
)
16934+
def test_aks_nodepool_add_with_secondary_network_interfaces(
16935+
self, resource_group, resource_group_location
16936+
):
16937+
aks_name = self.create_random_name("cliakstest", 16)
16938+
nodepool_name = self.create_random_name("n", 6)
16939+
16940+
self.kwargs.update(
16941+
{
16942+
"resource_group": resource_group,
16943+
"name": aks_name,
16944+
"location": resource_group_location,
16945+
"ssh_key_value": self.generate_ssh_keys(),
16946+
"node_pool_name": nodepool_name,
16947+
"node_vm_size": "standard_d4s_v3",
16948+
}
16949+
)
16950+
16951+
# Create a VNet with subnets for nodes and secondary NICs
16952+
self.cmd(
16953+
"network vnet create "
16954+
"--resource-group={resource_group} "
16955+
"--name=testvnet "
16956+
"--address-prefix 192.168.0.0/16",
16957+
)
16958+
self.cmd(
16959+
"network vnet subnet create "
16960+
"--resource-group={resource_group} "
16961+
"--vnet-name=testvnet "
16962+
"--name=nodesubnet "
16963+
"--address-prefix 192.168.0.0/24",
16964+
)
16965+
subnet = self.cmd(
16966+
"network vnet subnet create "
16967+
"--resource-group={resource_group} "
16968+
"--vnet-name=testvnet "
16969+
"--name=secondarysubnet "
16970+
"--address-prefix 192.168.1.0/24",
16971+
).get_output_in_json()
16972+
16973+
node_subnet_id = (
16974+
f"/subscriptions/{self.get_subscription_id()}"
16975+
f"/resourceGroups/{resource_group}"
16976+
"/providers/Microsoft.Network/virtualNetworks/testvnet/subnets/nodesubnet"
16977+
)
16978+
secondary_subnet_id = subnet["id"]
16979+
16980+
self.kwargs.update(
16981+
{
16982+
"node_subnet_id": node_subnet_id,
16983+
"secondary_nics": f'[{{"type":"Standard","vnetSubnetId":"{secondary_subnet_id}"}}]',
16984+
}
16985+
)
16986+
16987+
# Register the preview feature required for secondary NICs
16988+
self.cmd(
16989+
"feature register "
16990+
"--namespace Microsoft.ContainerService "
16991+
"--name NetworkingMultiNICPreview"
16992+
)
16993+
16994+
# Wait until the feature is registered
16995+
while True:
16996+
result = self.cmd(
16997+
"feature show "
16998+
"--namespace Microsoft.ContainerService "
16999+
"--name NetworkingMultiNICPreview"
17000+
).get_output_in_json()
17001+
if result["properties"]["state"] == "Registered":
17002+
break
17003+
time.sleep(30)
17004+
17005+
# Propagate the registration
17006+
self.cmd("provider register --namespace Microsoft.ContainerService")
17007+
17008+
# Create the cluster
17009+
self.cmd(
17010+
"aks create "
17011+
"--resource-group={resource_group} "
17012+
"--name={name} "
17013+
"--location={location} "
17014+
"--ssh-key-value={ssh_key_value} "
17015+
"--node-count=1 "
17016+
"--node-vm-size={node_vm_size} "
17017+
"--vnet-subnet-id={node_subnet_id} ",
17018+
checks=[
17019+
self.check("provisioningState", "Succeeded"),
17020+
],
17021+
)
17022+
17023+
# Add nodepool with secondary network interfaces
17024+
self.cmd(
17025+
"aks nodepool add "
17026+
"--resource-group={resource_group} "
17027+
"--cluster-name={name} "
17028+
"--name={node_pool_name} "
17029+
"--node-vm-size={node_vm_size} "
17030+
"--node-count=1 "
17031+
"--vnet-subnet-id={node_subnet_id} "
17032+
"--secondary-network-interfaces '{secondary_nics}' ",
17033+
checks=[
17034+
self.check("provisioningState", "Succeeded"),
17035+
self.check(
17036+
"networkProfile.secondaryNetworkInterfaces[0].vnetSubnetId",
17037+
secondary_subnet_id,
17038+
),
17039+
self.check(
17040+
"networkProfile.secondaryNetworkInterfaces[0].type",
17041+
"Standard",
17042+
),
17043+
],
17044+
)
17045+
17046+
# delete
17047+
cmd = (
17048+
"aks delete --resource-group={resource_group} --name={name} --yes --no-wait"
17049+
)
17050+
self.cmd(
17051+
cmd,
17052+
checks=[
17053+
self.is_empty(),
17054+
],
17055+
)
17056+
1692917057
@AllowLargeResponse()
1693017058
@AKSCustomResourceGroupPreparer(
1693117059
random_name_length=17, name_prefix="clitest", location="eastus"

0 commit comments

Comments
 (0)