From 37f37988fd64d8231ee2fab7215dbcfcd3d8012c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Dost=C3=A1l?= Date: Tue, 11 Aug 2026 13:10:27 +0000 Subject: [PATCH] [Compute] Fix #33556: `az vm boot-diagnostics get-boot-log`: Fix TypeError with azure-mgmt-storage 25.0.0 StorageAccountListKeysResult in azure-mgmt-storage 25.0.0 now inherits from MutableMapping, so `.keys` resolves to the built-in dict.keys method instead of the renamed `keys_property` list. This crashed get_boot_log when a VM uses a custom (non-managed) storage account for boot diagnostics. Also fixes the same pattern in the unused _get_private_config helper, and adds a unit test covering the keys_property credential path and the full download flow. Co-Authored-By: Claude Sonnet 5 --- .../azure/cli/command_modules/vm/custom.py | 4 +- .../tests/latest/test_custom_vm_commands.py | 42 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/vm/custom.py b/src/azure-cli/azure/cli/command_modules/vm/custom.py index b1b55aab17d..1f0799c4c0f 100644 --- a/src/azure-cli/azure/cli/command_modules/vm/custom.py +++ b/src/azure-cli/azure/cli/command_modules/vm/custom.py @@ -204,7 +204,7 @@ def _get_disk_lun_by_aaz(data_disks): def _get_private_config(cli_ctx, resource_group_name, storage_account): storage_mgmt_client = _get_storage_management_client(cli_ctx) # pylint: disable=no-member - keys = storage_mgmt_client.storage_accounts.list_keys(resource_group_name, storage_account).keys + keys = storage_mgmt_client.storage_accounts.list_keys(resource_group_name, storage_account).keys_property private_config = { 'storageAccountName': storage_account, @@ -2300,7 +2300,7 @@ def get_boot_log(cmd, resource_group_name, vm_name): # Get account key keys = storage_mgmt_client.storage_accounts.list_keys(rg, storage_account.name) - blob_client = BlobClient.from_blob_url(blob_url=blob_uri, credential=keys.keys[0].value) + blob_client = BlobClient.from_blob_url(blob_url=blob_uri, credential=keys.keys_property[0].value) # our streamwriter not seekable, so no parallel. downloader = blob_client.download_blob(max_concurrency=1) 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 0291e22490b..4ab2527a4aa 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 @@ -195,6 +195,48 @@ class ErrorToExitCommandEarly(Exception): except ErrorToExitCommandEarly: get_sdk_mock.assert_called_with(cli_ctx_mock, ResourceType.DATA_STORAGE_BLOB, '_blob_client#BlobClient') + @mock.patch('azure.cli.command_modules.vm.custom.BootLogStreamWriter', autospec=True) + @mock.patch('azure.cli.command_modules.vm.custom._get_storage_management_client', autospec=True) + @mock.patch('azure.cli.command_modules.vm.custom.get_instance_view', autospec=True) + @mock.patch('azure.cli.core.profiles.get_sdk', autospec=True) + def test_vm_boot_log_uses_keys_property(self, get_sdk_mock, get_instance_view_mock, + get_storage_client_mock, stream_writer_mock): + # azure-mgmt-storage>=25.0.0 renamed the account keys list from `keys` to + # `keys_property` (`.keys` now resolves to `MutableMapping.keys`). + blob_url = 'https://mystorage.blob.core.windows.net/bootdiagnostics/vm1.serialconsole.log' + get_instance_view_mock.return_value = { + 'instanceView': { + 'bootDiagnostics': {'serialConsoleLogBlobUri': blob_url} + } + } + + storage_account = mock.MagicMock() + storage_account.primary_endpoints.blob = 'https://mystorage.blob.core.windows.net/' + storage_account.id = '/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/mystorage' + storage_account.name = 'mystorage' + + storage_key = mock.MagicMock() + storage_key.value = 'the-account-key' + list_keys_result = mock.MagicMock(spec=['keys_property']) + list_keys_result.keys_property = [storage_key] + + storage_client_mock = get_storage_client_mock.return_value + storage_client_mock.storage_accounts.list.return_value = [storage_account] + storage_client_mock.storage_accounts.list_keys.return_value = list_keys_result + + blob_client_mock = mock.MagicMock() + blob_client_class_mock = mock.MagicMock() + blob_client_class_mock.from_blob_url.return_value = blob_client_mock + get_sdk_mock.return_value = blob_client_class_mock + + cmd_mock = mock.MagicMock() + get_boot_log(cmd_mock, 'rg1', 'vm1') + + blob_client_class_mock.from_blob_url.assert_called_once_with( + blob_url=blob_url, credential='the-account-key') + blob_client_mock.download_blob.assert_called_once_with(max_concurrency=1) + blob_client_mock.download_blob.return_value.readinto.assert_called_once() + class FakedVM: # pylint: disable=too-few-public-methods def __init__(self, nics=None, disks=None, os_disk=None):