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
1 change: 1 addition & 0 deletions src/aks-preview/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ To release a new version, please select a new version number (usually plus 1 to

Pending
+++++++
* `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.
Comment thread
jumpinthefire marked this conversation as resolved.
* `az aks create` and `az aks nodepool add`: Add `--enable-osdisk-full-caching` (preview) to enable the full-cache ephemeral OS disk feature for a node pool. Requires AFEC registration `Microsoft.ContainerService/FullCachePreview`. Property is immutable after node pool creation.

21.0.0b1
Expand Down
7 changes: 7 additions & 0 deletions src/aks-preview/azext_aks_preview/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -2291,6 +2291,13 @@
- name: --localdns-config
type: string
short-summary: Set the localDNS Profile for a nodepool with a JSON config file.
- name: --secondary-network-interfaces --secondary-nics
type: string
short-summary: Secondary network interface configurations as a JSON string or `@filename`.
long-summary: |-
Specify secondary NICs to attach to each node. Accepts inline JSON or `@filename`.
Example: '[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet","enableAcceleratedNetworking":true}]'
Supported NIC types are "Standard" (requires vnetSubnetId) and "Dynamic".
- name: --upgrade-strategy
type: string
short-summary: Upgrade strategy for the node pool. Allowed values are "Rolling" or "BlueGreen". Default is "Rolling".
Expand Down
8 changes: 8 additions & 0 deletions src/aks-preview/azext_aks_preview/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2208,6 +2208,14 @@ def load_arguments(self, _):
'localdns_config',
help='Path to a JSON file to configure the local DNS profile for a new nodepool.'
)
# secondary network interfaces
c.argument(
'secondary_network_interfaces',
options_list=['--secondary-network-interfaces', '--secondary-nics'],
help='Secondary network interface configurations as a JSON string or `@filename` to load from a file. '
'Example: \'[{"type":"Standard","vnetSubnetId":"/subscriptions/.../subnets/mysubnet"}]\'',
is_preview=True,
)
Comment thread
jumpinthefire marked this conversation as resolved.

with self.argument_context("aks nodepool update") as c:
c.argument(
Expand Down
38 changes: 38 additions & 0 deletions src/aks-preview/azext_aks_preview/agentpool_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from azure.cli.core.util import (
read_file_content,
sdk_no_wait,
shell_safe_json_parse,
)
from azure.core import MatchConditions
from knack.log import get_logger
Expand Down Expand Up @@ -961,6 +962,39 @@ def get_localdns_profile(self):
return profile
return None

def get_secondary_network_interfaces(self):
"""Obtain the value of secondary_network_interfaces.

Parse inline JSON or @file reference into a list of AgentPoolNetworkInterface models.
"""
raw = self.raw_param.get("secondary_network_interfaces")
if raw is None:
return None
if isinstance(raw, str):
if raw.startswith("@"):
data = get_file_json(raw[1:])
Comment thread
jumpinthefire marked this conversation as resolved.
else:
data = shell_safe_json_parse(raw)
else:
data = raw
if not isinstance(data, list):
raise InvalidArgumentValueError(
"--secondary-network-interfaces must be a JSON array."
)
result = []
for idx, item in enumerate(data):
if not isinstance(item, dict):
raise InvalidArgumentValueError(
f"--secondary-network-interfaces: element at index {idx} "
f"must be a JSON object, got {type(item).__name__}."
)
result.append(self.models.AgentPoolNetworkInterface(
type=item.get("type"),
vnet_subnet_id=item.get("vnetSubnetId"),
enable_accelerated_networking=item.get("enableAcceleratedNetworking"),
))
return result

def build_localdns_profile(self, agentpool: AgentPool) -> AgentPool:
"""Build local DNS profile for the AgentPool object if provided via --localdns-config."""
localdns_profile = self.get_localdns_profile()
Expand Down Expand Up @@ -1304,6 +1338,10 @@ def set_up_agentpool_network_profile(self, agentpool: AgentPool) -> AgentPool:
if ip_tags:
agentpool.network_profile.node_public_ip_tags = ip_tags

secondary_nics = self.context.get_secondary_network_interfaces()
if secondary_nics is not None:
agentpool.network_profile.secondary_network_interfaces = secondary_nics

return agentpool

def set_up_taints(self, agentpool: AgentPool) -> AgentPool:
Expand Down
2 changes: 2 additions & 0 deletions src/aks-preview/azext_aks_preview/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1990,6 +1990,8 @@ def aks_agentpool_add(
vm_sizes=None,
# local DNS
localdns_config=None,
# secondary network interfaces
secondary_network_interfaces=None,
):
# DO NOT MOVE: get all the original parameters and save them as a dictionary
raw_parameters = locals()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,86 @@ def common_get_final_soak_duration(self):
ctx_3.attach_agentpool(agentpool_3)
self.assertEqual(ctx_3.get_final_soak_duration(), 1200)

def common_get_secondary_network_interfaces(self):
# default - None
ctx_1 = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({"secondary_network_interfaces": None}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)
self.assertEqual(ctx_1.get_secondary_network_interfaces(), None)

# inline JSON
ctx_2 = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({
"secondary_network_interfaces": '[{"type":"Standard","vnetSubnetId":"/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1"}]'
}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)
result = ctx_2.get_secondary_network_interfaces()
self.assertIsNotNone(result)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].type, "Standard")
self.assertEqual(result[0].vnet_subnet_id, "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1")

# invalid JSON - not a list
ctx_3 = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({
"secondary_network_interfaces": '{"type":"Standard"}'
}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)
with self.assertRaises(InvalidArgumentValueError):
ctx_3.get_secondary_network_interfaces()

# invalid JSON - array element is not a dict
ctx_4 = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({
"secondary_network_interfaces": '[null]'
}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)
with self.assertRaises(InvalidArgumentValueError):
ctx_4.get_secondary_network_interfaces()

# @file input
import tempfile
import json
nics_data = [{"type": "Dynamic"}, {"type": "Standard", "vnetSubnetId": "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1"}]
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
json.dump(nics_data, f)
tmp_path = f.name
try:
ctx_5 = AKSPreviewAgentPoolContext(
self.cmd,
AKSAgentPoolParamDict({
"secondary_network_interfaces": f"@{tmp_path}"
}),
self.models,
DecoratorMode.CREATE,
self.agentpool_decorator_mode,
)
result = ctx_5.get_secondary_network_interfaces()
self.assertEqual(len(result), 2)
self.assertEqual(result[0].type, "Dynamic")
self.assertIsNone(result[0].vnet_subnet_id)
self.assertEqual(result[1].type, "Standard")
self.assertEqual(result[1].vnet_subnet_id, "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1")
finally:
import os
os.unlink(tmp_path)


class AKSPreviewAgentPoolContextStandaloneModeTestCase(
AKSPreviewAgentPoolContextCommonTestCase
Expand Down Expand Up @@ -1282,6 +1362,9 @@ def test_get_batch_soak_duration(self):
def test_get_final_soak_duration(self):
self.common_get_final_soak_duration()

def test_get_secondary_network_interfaces(self):
self.common_get_secondary_network_interfaces()


class AKSPreviewAgentPoolContextManagedClusterModeTestCase(
AKSPreviewAgentPoolContextCommonTestCase
Expand Down
107 changes: 107 additions & 0 deletions src/aks-preview/azext_aks_preview/tests/latest/test_aks_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -16906,6 +16906,113 @@ def test_aks_nodepool_create_with_nsg_control(
],
)

@live_only()
@AllowLargeResponse()
@AKSCustomResourceGroupPreparer(
random_name_length=17, name_prefix="clitest", location="westus2"
)
def test_aks_nodepool_add_with_secondary_network_interfaces(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you please queue a live test run to demonstrate that it can pass?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I ran a live test locally and it passed (after some adjustments):

⏺ Bash(AZURE_TEST_RUN_LIVE=True python -m pytest azext_aks_preview/tests/latest/test_aks_commands.py -k "test_aks_nodepool_add_with_secondary_network_interfaces" -x 2>…)
  ⎿  ========== 1 passed, 365 deselected, 12 warnings in 426.83s (0:07:06) ==========

I also added the @live_only decorator but let me know if you'd rather have a recording available.

self, resource_group, resource_group_location
):
aks_name = self.create_random_name("cliakstest", 16)
nodepool_name = 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_d4s_v3",
}
)

# Create a VNet with subnets for nodes and secondary NICs
self.cmd(
"network vnet create "
"--resource-group={resource_group} "
"--name=testvnet "
"--address-prefix 192.168.0.0/16",
)
self.cmd(
"network vnet subnet create "
"--resource-group={resource_group} "
"--vnet-name=testvnet "
"--name=nodesubnet "
"--address-prefix 192.168.0.0/24",
)
subnet = self.cmd(
"network vnet subnet create "
"--resource-group={resource_group} "
"--vnet-name=testvnet "
"--name=secondarysubnet "
"--address-prefix 192.168.1.0/24",
).get_output_in_json()

node_subnet_id = (
f"/subscriptions/{self.get_subscription_id()}"
f"/resourceGroups/{resource_group}"
"/providers/Microsoft.Network/virtualNetworks/testvnet/subnets/nodesubnet"
)
secondary_subnet_id = subnet["id"]

self.kwargs.update(
{
"node_subnet_id": node_subnet_id,
"secondary_nics": f'[{{"type":"Standard","vnetSubnetId":"{secondary_subnet_id}"}}]',
}
)

# Create the cluster
self.cmd(
"aks create "
"--resource-group={resource_group} "
"--name={name} "
"--location={location} "
"--ssh-key-value={ssh_key_value} "
"--node-count=1 "
"--node-vm-size={node_vm_size} "
"--vnet-subnet-id={node_subnet_id} ",
checks=[
self.check("provisioningState", "Succeeded"),
],
)

# Add nodepool with secondary network interfaces
self.cmd(
"aks nodepool add "
"--resource-group={resource_group} "
"--cluster-name={name} "
"--name={node_pool_name} "
"--node-vm-size={node_vm_size} "
"--node-count=1 "
"--vnet-subnet-id={node_subnet_id} "
"--secondary-network-interfaces '{secondary_nics}' ",
checks=[
self.check("provisioningState", "Succeeded"),
self.check(
"networkProfile.secondaryNetworkInterfaces[0].vnetSubnetId",
secondary_subnet_id,
),
self.check(
"networkProfile.secondaryNetworkInterfaces[0].type",
"Standard",
),
],
)

# delete
cmd = (
"aks delete --resource-group={resource_group} --name={name} --yes --no-wait"
)
self.cmd(
cmd,
checks=[
self.is_empty(),
],
)

@AllowLargeResponse()
@AKSCustomResourceGroupPreparer(
random_name_length=17, name_prefix="clitest", location="eastus"
Expand Down
Loading