Skip to content

Commit 56dde0c

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

2 files changed

Lines changed: 160 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: 156 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,157 @@ 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+
235+
def test_vmss_update_include_to_exclude(self):
236+
for include_zones in (['1', '2'], [], None):
237+
with self.subTest(include_zones=include_zones):
238+
self._assert_update(
239+
{'placement': {
240+
'zonePlacementPolicy': 'Auto', 'includeZones': include_zones, 'excludeZones': None
241+
}},
242+
{'placement': {'zone_placement_policy': 'Auto', 'exclude_zones': ['3']}},
243+
exclude_zones=['3'])
244+
245+
def test_vmss_update_exclude_to_include(self):
246+
for exclude_zones in (['3'], [], None):
247+
with self.subTest(exclude_zones=exclude_zones):
248+
self._assert_update(
249+
{'placement': {
250+
'zonePlacementPolicy': 'Auto', 'excludeZones': exclude_zones, 'includeZones': None
251+
}},
252+
{'placement': {'zone_placement_policy': 'Auto', 'include_zones': ['1', '2']}},
253+
include_zones=['1', '2'])
254+
255+
def test_vmss_update_same_filter(self):
256+
for persisted_name, argument_name in (('includeZones', 'include_zones'), ('excludeZones', 'exclude_zones')):
257+
for requested_zones in (['1', '2'], ['2', '3']):
258+
with self.subTest(argument=argument_name, requested_zones=requested_zones):
259+
self._assert_update(
260+
{'placement': {'zonePlacementPolicy': 'Auto', persisted_name: ['1', '2']}},
261+
{'placement': {'zone_placement_policy': 'Auto', argument_name: requested_zones}},
262+
**{argument_name: requested_zones})
263+
264+
def test_vmss_update_empty_filter_is_explicit(self):
265+
# CLI flags require values (nargs='+'); an empty list is still explicit at the handler boundary.
266+
for persisted_name, argument_name in (('includeZones', 'exclude_zones'), ('excludeZones', 'include_zones')):
267+
with self.subTest(argument=argument_name):
268+
self._assert_update(
269+
{'placement': {'zonePlacementPolicy': 'Auto', persisted_name: ['1', '2']}},
270+
{'placement': {'zone_placement_policy': 'Auto', argument_name: []}},
271+
**{argument_name: []})
272+
273+
def test_vmss_update_without_filters_preserves_placement(self):
274+
for persisted_filter, expected_filter in (
275+
({'includeZones': ['1', '2'], 'excludeZones': None},
276+
{'include_zones': ['1', '2'], 'exclude_zones': None}),
277+
({'excludeZones': ['3'], 'includeZones': None},
278+
{'exclude_zones': ['3'], 'include_zones': None})):
279+
for updates in ({}, {'include_zones': None, 'exclude_zones': None}, {'zone_placement_policy': 'Auto'}):
280+
with self.subTest(persisted_filter=persisted_filter, updates=updates):
281+
self._assert_update(
282+
{'placement': {'zonePlacementPolicy': 'Auto', **persisted_filter}},
283+
{'placement': {'zone_placement_policy': 'Auto', **expected_filter}},
284+
**updates)
285+
286+
def test_vmss_update_unrelated_property_preserves_placement(self):
287+
for persisted_name, argument_name in (('includeZones', 'include_zones'), ('excludeZones', 'exclude_zones')):
288+
with self.subTest(filter=argument_name):
289+
self._assert_update(
290+
{'placement': {'zonePlacementPolicy': 'Auto', persisted_name: ['1', '2']}},
291+
{
292+
'placement': {'zone_placement_policy': 'Auto', argument_name: ['1', '2']},
293+
'sku': {'name': 'Standard_D4s_v5', 'tier': 'Standard', 'capacity': 0},
294+
'no_wait': True
295+
},
296+
vm_sku='Standard_D4s_v5', no_wait=True)
297+
298+
def test_vmss_update_absent_or_null_placement(self):
299+
for persisted in ({}, {'placement': None}, {'placement': {}}):
300+
with self.subTest(persisted=persisted):
301+
self._assert_update(persisted, persisted)
302+
self._assert_update(persisted, persisted, include_zones=None, exclude_zones=None)
303+
for updates in (
304+
{'zone_placement_policy': 'Auto'},
305+
{'zone_placement_policy': 'Auto', 'include_zones': ['1', '2']},
306+
{'zone_placement_policy': 'Auto', 'exclude_zones': ['3']}):
307+
with self.subTest(updates=updates):
308+
self._assert_update(persisted, {'placement': updates}, **updates)
309+
310+
def test_vmss_update_policy_only_preserves_filter(self):
311+
for persisted_name, argument_name in (('includeZones', 'include_zones'), ('excludeZones', 'exclude_zones')):
312+
with self.subTest(filter=argument_name):
313+
self._assert_update(
314+
{'placement': {persisted_name: ['1', '2']}},
315+
{'placement': {'zone_placement_policy': 'Auto', argument_name: ['1', '2']}},
316+
zone_placement_policy='Auto')
317+
318+
def test_vmss_update_rejects_both_filters(self):
319+
namespace = Namespace(include_zones=['1', '2'], exclude_zones=['3'])
320+
with self.assertRaisesRegex(MutuallyExclusiveArgumentError,
321+
'You can only specify one of --include-zones and --exclude-zones'):
322+
_validate_vmss_auto_zone_placement(namespace)
323+
324+
170325
class TestVMBootLog(unittest.TestCase):
171326

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

0 commit comments

Comments
 (0)