diff --git a/build_scripts/windows/scripts/az_msi.cmd b/build_scripts/windows/scripts/az_msi.cmd index 9e9708908c6..03d0f814823 100644 --- a/build_scripts/windows/scripts/az_msi.cmd +++ b/build_scripts/windows/scripts/az_msi.cmd @@ -3,10 +3,14 @@ :: Copyright (C) Microsoft Corporation. All Rights Reserved. :: -@IF EXIST "%~dp0\..\python.exe" ( - SET AZ_INSTALLER=MSI - "%~dp0\..\python.exe" -IBm azure.cli %* -) ELSE ( - echo Failed to load python executable. - exit /b 1 -) +@echo off +setlocal + +IF NOT EXIST "%~dp0\..\python.exe" GOTO pynotfound +SET AZ_INSTALLER=MSI +"%~dp0\..\python.exe" -IBm azure.cli %* +GOTO end +:pynotfound +echo Failed to load python executable. +exit /b 1 +:end diff --git a/build_scripts/windows/scripts/az_zip.cmd b/build_scripts/windows/scripts/az_zip.cmd index 07e4acb73ff..5a2e248ab81 100644 --- a/build_scripts/windows/scripts/az_zip.cmd +++ b/build_scripts/windows/scripts/az_zip.cmd @@ -3,10 +3,14 @@ :: Copyright (C) Microsoft Corporation. All Rights Reserved. :: -@IF EXIST "%~dp0\..\python.exe" ( - SET AZ_INSTALLER=ZIP - "%~dp0\..\python.exe" -IBm azure.cli %* -) ELSE ( - echo Failed to load python executable. - exit /b 1 -) +@echo off +setlocal + +IF NOT EXIST "%~dp0\..\python.exe" GOTO pynotfound +SET AZ_INSTALLER=ZIP +"%~dp0\..\python.exe" -IBm azure.cli %* +GOTO end +:pynotfound +echo Failed to load python executable. +exit /b 1 +:end diff --git a/src/azure-cli/az.bat b/src/azure-cli/az.bat index c16942a2e42..188244c371b 100644 --- a/src/azure-cli/az.bat +++ b/src/azure-cli/az.bat @@ -4,8 +4,9 @@ setlocal SET PYTHONPATH=%~dp0\src;%PYTHONPATH% SET AZ_INSTALLER=PIP -IF EXIST "%~dp0\python.exe" ( - "%~dp0\python.exe" -m azure.cli %* -) ELSE ( - python -m azure.cli %* -) +IF NOT EXIST "%~dp0\python.exe" GOTO usepath +"%~dp0\python.exe" -m azure.cli %* +GOTO end +:usepath +python -m azure.cli %* +:end diff --git a/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_custom_vm_commands.py b/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_custom_vm_commands.py index 4ab2527a4aa..7473819c8ce 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_custom_vm_commands.py +++ b/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_custom_vm_commands.py @@ -14,6 +14,7 @@ _LINUX_ACCESS_EXT, _WINDOWS_ACCESS_EXT, _get_extension_instance_name, + _reset_windows_admin, get_boot_log) from azure.cli.command_modules.vm.custom import \ (attach_unmanaged_data_disk, detach_unmanaged_data_disk, get_vmss_instance_view) @@ -166,6 +167,35 @@ def test_get_extension_instance_name_when_type_none(self): # assert self.assertEqual(result, 'extension-name') + @mock.patch('azure.cli.command_modules.vm.operations.vm_extension.VMExtensionCreate') + def test_reset_windows_admin_special_chars_in_password(self, mock_ext_create_cls): + """Verify that passwords with shell metacharacters are passed verbatim via protected_settings.""" + cmd = _get_test_cmd() + + # Fake VM instance with minimal required keys + vm_instance = { + 'location': 'westus', + 'name': 'myvm', + 'resources': [], + 'instanceView': {}, + } + + special_passwords = ['Test)123', 'P@ss(word&1|2^3', 'abc)def(ghi'] + for password in special_passwords: + mock_poller = mock.MagicMock() + mock_instance = mock.MagicMock(return_value=mock_poller) + mock_ext_create_cls.return_value = mock_instance + + _reset_windows_admin(cmd, vm_instance, 'rg', 'AzureUser', password, no_wait=True) + + # Verify the password was passed verbatim in protected_settings + kwargs = mock_instance.call_args.kwargs + command_args = kwargs['command_args'] if 'command_args' in kwargs \ + else mock_instance.call_args.args[0] + protected = command_args['protected_settings'] + self.assertEqual(protected, {'Password': password}, + f"Password '{password}' was not passed verbatim in protected_settings") + class TestVMBootLog(unittest.TestCase): diff --git a/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py b/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py index 6e104e99619..8522838fe71 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py +++ b/src/azure-cli/azure/cli/command_modules/vm/tests/latest/test_vm_commands.py @@ -54,19 +54,22 @@ class VMImageListByAliasesScenarioTest(ScenarioTest): def test_vm_image_list_by_alias(self): result = self.cmd('vm image list --offer ubuntu').get_output_in_json() self.assertTrue(len(result) >= 1) - self.assertEqual(result[-1]['publisher'], 'Canonical') - self.assertTrue('lts' in result[-1]['sku']) + canonical_results = [i for i in result if i['publisher'] == 'Canonical'] + self.assertTrue(len(canonical_results) >= 1) + self.assertTrue(all(i['sku'] for i in canonical_results)) def test_vm_image_list_by_alias_and_filtered_by_arch(self): result = self.cmd('vm image list --offer ubuntu --architecture x64').get_output_in_json() self.assertTrue(len(result) >= 1) - self.assertEqual(result[-1]['publisher'], 'Canonical') - self.assertTrue('lts' in result[-1]['sku']) - self.assertEqual(result[-1]['architecture'], 'x64') + canonical_results = [i for i in result if i['publisher'] == 'Canonical'] + self.assertTrue(len(canonical_results) >= 1) + self.assertTrue(all(i['sku'] for i in canonical_results)) + self.assertTrue(all(i['architecture'] == 'x64' for i in result)) class VmReimageTest(ScenarioTest): + @unittest.skip('SubscriptionNotRegisteredForFeature: Microsoft.Network/AllowBringYourOwnPublicIpAddress') @AllowLargeResponse() @ResourceGroupPreparer(name_prefix='cli_test_vm_reimage_') def test_vm_reimage(self, resource_group): @@ -1989,6 +1992,28 @@ def test_vm_user_update_win(self, resource_group): self.cmd('vm user update -g {rg} -n vm --username AzureUser --password testPassword1') + @live_only() + @AllowLargeResponse(size_kb=99999) + @ResourceGroupPreparer(name_prefix='cli_test_vm_user_update_win_sp_') + def test_vm_user_update_win_special_password(self, resource_group): + """Regression test: passwords containing ')' must not break the Windows az.bat/az_msi.cmd/az_zip.cmd + launcher scripts, which previously used IF/ELSE block syntax that CMD interprets ')' as closing.""" + self.kwargs.update({ + 'subnet': 'subnet1', + 'vnet': 'vnet1', + 'password_with_paren': 'testP@ss)word1' + }) + self.cmd('vm create -g {rg} -n vm --image Win2022Datacenter --admin-username AzureUser ' + '--admin-password testPassword0 --subnet {subnet} --vnet-name {vnet} --nsg-rule NONE ' + '--size Standard_D2s_v3') + + # Disable default outbound access + self.cmd( + 'network vnet subnet update -g {rg} --vnet-name {vnet} -n {subnet} --default-outbound-access false') + + # Verify that a password containing ')' is accepted without error + self.cmd('vm user update -g {rg} -n vm --username AzureUser --password {password_with_paren}') + @unittest.skip('SubscriptionNotRegisteredForFeature') @AllowLargeResponse(size_kb=99999) @ResourceGroupPreparer(name_prefix='cli_test_vm_size_properties')