Skip to content

Commit bc1e97a

Browse files
committed
[VM] Fix #34063: az vmss update: Clear the opposite zone placement filter when switching filters
1 parent d295b94 commit bc1e97a

3 files changed

Lines changed: 180 additions & 1 deletion

File tree

‎src/azure-cli/azure/cli/command_modules/vm/custom.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5150,9 +5150,13 @@ def _output(self, *args, **kwargs):
51505150

51515151
if include_zones is not None:
51525152
vmss["placement"]["include_zones"] = include_zones
5153+
if exclude_zones is None:
5154+
vmss["placement"].pop("exclude_zones", None)
51535155

51545156
if exclude_zones is not None:
51555157
vmss["placement"]["exclude_zones"] = exclude_zones
5158+
if include_zones is None:
5159+
vmss["placement"].pop("include_zones", None)
51565160

51575161
from .operations.vmss import VMSSCreate
51585162
return VMSSCreate(cli_ctx=cmd.cli_ctx)(command_args=vmss)

‎src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_custom_vm_commands.py‎

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@
44
# --------------------------------------------------------------------------------------------
55

66
import unittest
7+
from argparse import Namespace
8+
from copy import deepcopy
79
from unittest import mock
810

911
from knack.util import CLIError
1012

13+
from azure.cli.command_modules.vm._validators import _validate_vmss_auto_zone_placement
1114
from azure.cli.command_modules.vm.custom import (enable_boot_diagnostics, disable_boot_diagnostics,
1215
_merge_secrets, BootLogStreamWriter,
1316
_get_access_extension_upgrade_info,
1417
_LINUX_ACCESS_EXT,
1518
_WINDOWS_ACCESS_EXT,
1619
_get_extension_instance_name,
17-
get_boot_log)
20+
get_boot_log, update_vmss)
1821
from azure.cli.command_modules.vm.custom import \
1922
(attach_unmanaged_data_disk, detach_unmanaged_data_disk, get_vmss_instance_view)
2023

2124
from azure.cli.core import AzCommandsLoader
25+
from azure.cli.core.azclierror import MutuallyExclusiveArgumentError
2226
from azure.cli.core.commands import AzCliCommand
2327

2428

@@ -167,6 +171,175 @@ def test_get_extension_instance_name_when_type_none(self):
167171
self.assertEqual(result, 'extension-name')
168172

169173

174+
class TestVMSSUpdateZonePlacement(unittest.TestCase):
175+
176+
def setUp(self):
177+
self.cmd = _get_test_cmd()
178+
self.parameters = {
179+
'name': 'vmss',
180+
'location': 'eastus2',
181+
'tags': {'purpose': 'zone-placement'},
182+
'sku': {'name': 'Standard_D2s_v5', 'tier': 'Standard', 'capacity': 0},
183+
'orchestrationMode': 'Flexible',
184+
'platformFaultDomainCount': 1,
185+
'singlePlacementGroup': False,
186+
'upgradePolicy': {'mode': 'Manual'},
187+
'resiliencyPolicy': {'zoneAllocationPolicy': {'maxZoneCount': 2}},
188+
'virtualMachineProfile': {
189+
'osProfile': {'computerNamePrefix': 'vmss', 'adminUsername': 'azureuser'},
190+
'storageProfile': {
191+
'imageReference': {
192+
'publisher': 'Canonical',
193+
'offer': '0001-com-ubuntu-server-jammy',
194+
'sku': '22_04-lts-gen2',
195+
'version': 'latest'
196+
}
197+
}
198+
}
199+
}
200+
self.expected_args = {
201+
'resource_group': 'rg',
202+
'vm_scale_set_name': 'vmss',
203+
'no_wait': False,
204+
'location': 'eastus2',
205+
'tags': {'purpose': 'zone-placement'},
206+
'sku': {'name': 'Standard_D2s_v5', 'tier': 'Standard', 'capacity': 0},
207+
'orchestration_mode': 'Flexible',
208+
'platform_fault_domain_count': 1,
209+
'single_placement_group': False,
210+
'upgrade_policy': {'mode': 'Manual'},
211+
'resiliency_policy': {'zone_allocation_policy': {'max_zone_count': 2}},
212+
'virtual_machine_profile': {
213+
'os_profile': {'computer_name_prefix': 'vmss', 'admin_username': 'azureuser'},
214+
'storage_profile': {
215+
'image_reference': {
216+
'publisher': 'Canonical',
217+
'offer': '0001-com-ubuntu-server-jammy',
218+
'sku': '22_04-lts-gen2',
219+
'version': 'latest'
220+
}
221+
}
222+
}
223+
}
224+
225+
def _assert_update(self, persisted_properties, expected_properties, **updates):
226+
parameters = deepcopy({**self.parameters, **persisted_properties})
227+
expected_args = {**self.expected_args, **expected_properties}
228+
with mock.patch('azure.cli.command_modules.vm.operations.vmss.VMSSCreate') as vmss_create:
229+
result = update_vmss(self.cmd, 'rg', 'vmss', parameters=parameters, **updates)
230+
231+
vmss_create.assert_called_once_with(cli_ctx=self.cmd.cli_ctx)
232+
vmss_create.return_value.assert_called_once_with(command_args=expected_args)
233+
self.assertIs(result, vmss_create.return_value.return_value)
234+
return vmss_create.return_value.call_args.kwargs['command_args']
235+
236+
def _assert_serialized_placement(self, command_args, expected_placement):
237+
from azure.cli.command_modules.vm.operations.vmss import VMSSCreate
238+
from azure.cli.core.aaz._command_ctx import AAZCommandCtx
239+
240+
ctx = AAZCommandCtx(
241+
cli_ctx=self.cmd.cli_ctx,
242+
schema=VMSSCreate._build_arguments_schema(),
243+
command_args=command_args)
244+
# Exercise the generated PUT serializer without creating an authenticated client.
245+
with mock.patch.object(ctx, 'get_http_client'):
246+
operation = VMSSCreate.VirtualMachineScaleSetsCreateOrUpdate(ctx)
247+
self.assertEqual(operation.content['placement'], expected_placement)
248+
249+
def test_vmss_update_include_to_exclude(self):
250+
for include_zones in (['1', '2'], [], None):
251+
with self.subTest(include_zones=include_zones):
252+
command_args = self._assert_update(
253+
{'placement': {
254+
'zonePlacementPolicy': 'Auto', 'includeZones': include_zones, 'excludeZones': None
255+
}},
256+
{'placement': {'zone_placement_policy': 'Auto', 'exclude_zones': ['3']}},
257+
exclude_zones=['3'])
258+
self._assert_serialized_placement(
259+
command_args, {'zonePlacementPolicy': 'Auto', 'excludeZones': ['3']})
260+
261+
def test_vmss_update_exclude_to_include(self):
262+
for exclude_zones in (['3'], [], None):
263+
with self.subTest(exclude_zones=exclude_zones):
264+
command_args = self._assert_update(
265+
{'placement': {
266+
'zonePlacementPolicy': 'Auto', 'excludeZones': exclude_zones, 'includeZones': None
267+
}},
268+
{'placement': {'zone_placement_policy': 'Auto', 'include_zones': ['1', '2']}},
269+
include_zones=['1', '2'])
270+
self._assert_serialized_placement(
271+
command_args, {'zonePlacementPolicy': 'Auto', 'includeZones': ['1', '2']})
272+
273+
def test_vmss_update_same_filter(self):
274+
for persisted_name, argument_name in (('includeZones', 'include_zones'), ('excludeZones', 'exclude_zones')):
275+
for requested_zones in (['1', '2'], ['2', '3']):
276+
with self.subTest(argument=argument_name, requested_zones=requested_zones):
277+
self._assert_update(
278+
{'placement': {'zonePlacementPolicy': 'Auto', persisted_name: ['1', '2']}},
279+
{'placement': {'zone_placement_policy': 'Auto', argument_name: requested_zones}},
280+
**{argument_name: requested_zones})
281+
282+
def test_vmss_update_empty_filter_is_explicit(self):
283+
# CLI flags require values (nargs='+'); an empty list is still explicit at the handler boundary.
284+
for persisted_name, argument_name in (('includeZones', 'exclude_zones'), ('excludeZones', 'include_zones')):
285+
with self.subTest(argument=argument_name):
286+
self._assert_update(
287+
{'placement': {'zonePlacementPolicy': 'Auto', persisted_name: ['1', '2']}},
288+
{'placement': {'zone_placement_policy': 'Auto', argument_name: []}},
289+
**{argument_name: []})
290+
291+
def test_vmss_update_without_filters_preserves_placement(self):
292+
for persisted_filter, expected_filter in (
293+
({'includeZones': ['1', '2'], 'excludeZones': None},
294+
{'include_zones': ['1', '2'], 'exclude_zones': None}),
295+
({'excludeZones': ['3'], 'includeZones': None},
296+
{'exclude_zones': ['3'], 'include_zones': None})):
297+
for updates in ({}, {'include_zones': None, 'exclude_zones': None}, {'zone_placement_policy': 'Auto'}):
298+
with self.subTest(persisted_filter=persisted_filter, updates=updates):
299+
self._assert_update(
300+
{'placement': {'zonePlacementPolicy': 'Auto', **persisted_filter}},
301+
{'placement': {'zone_placement_policy': 'Auto', **expected_filter}},
302+
**updates)
303+
304+
def test_vmss_update_unrelated_property_preserves_placement(self):
305+
for persisted_name, argument_name in (('includeZones', 'include_zones'), ('excludeZones', 'exclude_zones')):
306+
with self.subTest(filter=argument_name):
307+
self._assert_update(
308+
{'placement': {'zonePlacementPolicy': 'Auto', persisted_name: ['1', '2']}},
309+
{
310+
'placement': {'zone_placement_policy': 'Auto', argument_name: ['1', '2']},
311+
'sku': {'name': 'Standard_D4s_v5', 'tier': 'Standard', 'capacity': 0},
312+
'no_wait': True
313+
},
314+
vm_sku='Standard_D4s_v5', no_wait=True)
315+
316+
def test_vmss_update_absent_or_null_placement(self):
317+
for persisted in ({}, {'placement': None}, {'placement': {}}):
318+
with self.subTest(persisted=persisted):
319+
self._assert_update(persisted, persisted)
320+
self._assert_update(persisted, persisted, include_zones=None, exclude_zones=None)
321+
for updates in (
322+
{'zone_placement_policy': 'Auto'},
323+
{'zone_placement_policy': 'Auto', 'include_zones': ['1', '2']},
324+
{'zone_placement_policy': 'Auto', 'exclude_zones': ['3']}):
325+
with self.subTest(updates=updates):
326+
self._assert_update(persisted, {'placement': updates}, **updates)
327+
328+
def test_vmss_update_policy_only_preserves_filter(self):
329+
for persisted_name, argument_name in (('includeZones', 'include_zones'), ('excludeZones', 'exclude_zones')):
330+
with self.subTest(filter=argument_name):
331+
self._assert_update(
332+
{'placement': {persisted_name: ['1', '2']}},
333+
{'placement': {'zone_placement_policy': 'Auto', argument_name: ['1', '2']}},
334+
zone_placement_policy='Auto')
335+
336+
def test_vmss_update_rejects_both_filters(self):
337+
namespace = Namespace(include_zones=['1', '2'], exclude_zones=['3'])
338+
with self.assertRaisesRegex(MutuallyExclusiveArgumentError,
339+
'You can only specify one of --include-zones and --exclude-zones'):
340+
_validate_vmss_auto_zone_placement(namespace)
341+
342+
170343
class TestVMBootLog(unittest.TestCase):
171344

172345
@mock.patch('azure.cli.command_modules.vm.custom.logger.warning')

‎src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15258,6 +15258,8 @@ def test_vmss_update_zone_placement_policy(self, resource_group):
1525815258
'vm_sku': 'Standard_D2s_v7'
1525915259
})
1526015260

15261+
# Cross-filter transitions are covered by TestVMSSUpdateZonePlacement; adding
15262+
# them here requires a successful live recording with VMSS Placement preview.
1526115263
# create a vmss (with no instances so no zones are in use) and update its include zones
1526215264
self.cmd('vmss create -g {rg} -n {vmss_include} -l {location} --image {image} '
1526315265
'--admin-username {admin_username} --admin-password {admin_password} --upgrade-policy-mode Manual '

0 commit comments

Comments
 (0)