|
4 | 4 | # -------------------------------------------------------------------------------------------- |
5 | 5 |
|
6 | 6 | import unittest |
| 7 | +from argparse import Namespace |
| 8 | +from copy import deepcopy |
7 | 9 | from unittest import mock |
8 | 10 |
|
9 | 11 | from knack.util import CLIError |
10 | 12 |
|
| 13 | +from azure.cli.command_modules.vm._validators import _validate_vmss_auto_zone_placement |
11 | 14 | from azure.cli.command_modules.vm.custom import (enable_boot_diagnostics, disable_boot_diagnostics, |
12 | 15 | _merge_secrets, BootLogStreamWriter, |
13 | 16 | _get_access_extension_upgrade_info, |
14 | 17 | _LINUX_ACCESS_EXT, |
15 | 18 | _WINDOWS_ACCESS_EXT, |
16 | 19 | _get_extension_instance_name, |
17 | | - get_boot_log) |
| 20 | + get_boot_log, update_vmss) |
18 | 21 | from azure.cli.command_modules.vm.custom import \ |
19 | 22 | (attach_unmanaged_data_disk, detach_unmanaged_data_disk, get_vmss_instance_view) |
20 | 23 |
|
21 | 24 | from azure.cli.core import AzCommandsLoader |
| 25 | +from azure.cli.core.azclierror import MutuallyExclusiveArgumentError |
22 | 26 | from azure.cli.core.commands import AzCliCommand |
23 | 27 |
|
24 | 28 |
|
@@ -167,6 +171,157 @@ def test_get_extension_instance_name_when_type_none(self): |
167 | 171 | self.assertEqual(result, 'extension-name') |
168 | 172 |
|
169 | 173 |
|
| 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 | + |
170 | 325 | class TestVMBootLog(unittest.TestCase): |
171 | 326 |
|
172 | 327 | @mock.patch('azure.cli.command_modules.vm.custom.logger.warning') |
|
0 commit comments