Skip to content

{Compute} az vm identity: Migrate commands to aaz-based implementation#32572

Merged
yanzhudd merged 44 commits intoAzure:devfrom
william051200:vm-identity-migration
Jan 12, 2026
Merged

{Compute} az vm identity: Migrate commands to aaz-based implementation#32572
yanzhudd merged 44 commits intoAzure:devfrom
william051200:vm-identity-migration

Conversation

@william051200
Copy link
Copy Markdown
Member

@william051200 william051200 commented Dec 23, 2025

Related command
az vm identity assign
az vm identity remove
az vm identity show

Description

Migration from mgmt.compute to aaz-based

Testing Guide

History Notes


This checklist is used to make sure that common guidelines for a pull request are followed.

Copilot AI review requested due to automatic review settings December 23, 2025 04:22
@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd bot commented Dec 23, 2025

️✔️AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
️✔️vm
️✔️latest
️✔️3.12
️✔️3.13

@azure-client-tools-bot-prd
Copy link
Copy Markdown

azure-client-tools-bot-prd bot commented Dec 23, 2025

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@yonzhan
Copy link
Copy Markdown
Collaborator

yonzhan commented Dec 23, 2025

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link
Copy Markdown

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

@william051200
Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the az vm identity commands (assign, remove, and show) from the older mgmt.compute SDK to the AAZ (Azure AutoRest) framework. The migration modernizes the command implementation while maintaining backward compatibility.

Key Changes

  • Migrated show_vm_identity, assign_vm_identity, and remove_vm_identity to use AAZ-based operations instead of mgmt.compute SDK
  • Added helper functions assign_identity and resolve_role_id to _vm_utils.py to support the new implementation
  • Introduced IdentityType enum for better type management
  • Created VMIdentityRemove class in operations/vm.py to handle identity removal logic
  • Updated test commands to include --size parameter for VM creation

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

File Description
test_vm_commands.py Updated test cases with explicit VM size specifications and reformatted command strings for better readability. No functional test changes.
operations/vm.py Added VMIdentityRemove class extending _VMPatch to handle identity removal with custom content formatting logic.
custom.py Migrated three identity functions to use AAZ operations: show_vm_identity, assign_vm_identity, and remove_vm_identity. Added get_vm_by_aaz helper and _remove_identities_by_aaz function.
_vm_utils.py Added utility functions (assign_identity, resolve_role_id, _gen_guid) and IdentityType enum to support AAZ-based identity operations. Consolidated imports at the top.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}

from ._vm_utils import IdentityType
if vm.get('identity') and vm.get('identity').get('type') == IdentityType.USER_ASSIGNED.value:
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a redundant 'UserAssigned' literal string being added to the mi_user_assigned list. This appears to be a workaround for special handling in VMIdentityRemove._format_content (line 194), but it creates confusion since 'UserAssigned' is not a valid identity resource ID. Consider documenting this special case with a comment explaining why 'UserAssigned' is added.

Suggested change
if vm.get('identity') and vm.get('identity').get('type') == IdentityType.USER_ASSIGNED.value:
if vm.get('identity') and vm.get('identity').get('type') == IdentityType.USER_ASSIGNED.value:
# NOTE: The literal 'UserAssigned' is intentionally appended as a marker for
# VMIdentityRemove._format_content, which uses it to apply special handling
# for purely user-assigned identities. It is not a real identity resource ID.

Copilot uses AI. Check for mistakes.
Comment on lines +2614 to +2621
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list() call is unnecessary here as dict.keys() already returns an iterable that can be directly used in the list comprehension. This creates an extra intermediate list.

Suggested change
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
(list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys()) +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())

Copilot uses AI. Check for mistakes.
Comment on lines +2614 to +2621
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list() call is unnecessary here as dict.keys() already returns an iterable that can be directly used in the list comprehension. This creates an extra intermediate list.

Suggested change
([key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())] +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
[key for key in list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())]
(list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys()) +
['UserAssigned'])
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED.value:
command_args['mi_user_assigned'] = []
command_args['mi_system_assigned'] = 'True'
elif vm.get('identity') and vm.get('identity').get('type') == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED.value:
command_args['mi_user_assigned'] = \
list(vm.get('identity', {}).get('userAssignedIdentities', {}).keys())

Copilot uses AI. Check for mistakes.
for key in list(identities.keys()):
identities[key] = None

if len(content.get('identity', {}).get('userAssignedIdentities', {}).keys()) < 1:
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code checks if the length is less than 1, but this is equivalent to checking if the dictionary is empty. Using if not content.get('identity', {}).get('userAssignedIdentities', {}) would be more idiomatic and readable.

Suggested change
if len(content.get('identity', {}).get('userAssignedIdentities', {}).keys()) < 1:
if not content.get('identity', {}).get('userAssignedIdentities', {}):

Copilot uses AI. Check for mistakes.

# create role assignment:
if identity_scope:
principal_id = resource.get('identity', {}).get('principal_id')
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key name 'principal_id' should be 'principalId' to match the camelCase format used in AAZ-based responses. Throughout the codebase (such as in assign_vm_identity at line 900), 'principalId' is used consistently when accessing AAZ response dictionaries.

Suggested change
principal_id = resource.get('identity', {}).get('principal_id')
principal_id = resource.get('identity', {}).get('principalId') or \
resource.get('identity', {}).get('principal_id')

Copilot uses AI. Check for mistakes.
if identity and not identity.get('userAssignedIdentities'):
identity['userAssignedIdentities'] = None

return identity or None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to consider whether this should return an empty dictionary ({}) or None when there is no identity on the VM. We can refer to the SDK behavior for guidance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Here is the output from SDK behavior, it returns None instead of of ( { } ) for empty result.

identity_types = ResourceIdentityType.system_assigned_user_assigned
elif vm.identity and vm.identity.type == ResourceIdentityType.user_assigned and enable_local_identity:
identity_types = ResourceIdentityType.system_assigned_user_assigned
if vm.get('identity') and vm.get('identity').get('type') == system_assigned_user_assigned:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for your reference, adding a default value to the get() function of dict might be more concise in this case

Suggested change
if vm.get('identity') and vm.get('identity').get('type') == system_assigned_user_assigned:
if vm.get('identity', {}).get('type', None) == system_assigned_user_assigned:

Comment on lines +847 to +849
system_assigned = "SystemAssigned"
user_assigned = "UserAssigned"
system_assigned_user_assigned = "SystemAssigned, UserAssigned"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since they are already defined in _vm_utils.py file, it would be better to leverage the definition across all the places that need to read the values

Comment on lines +205 to +209
if not identity.get('principalId'):
identity['principalId'] = None

if not identity.get('tenantId'):
identity['tenantId'] = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may I ask why we need to add the None value to the result?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just validated, I was using them when running some test, it is now redundant. I will be removing those lines. Thank you.

Comment on lines +241 to +242
if 'UserAssigned' in identities.keys():
identities.pop('UserAssigned')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I ask what the identities look like in this case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I have implemented this solution is because:
image

  1. VM Patch will always set userAssigned and systemAssigned action flag to create.
  2. VM Patch only accept a list of userAssigned identities, then AAZIdentityObject will always format the identity to create based on the create action.
  3. Therefore, I will only pass identity to be removed to the list as I did a little twist by inheriting the original VM Patch, then set the identities to be removed into this format {'identity':None}. (I found this is the only format to remove userAssigned identity from the list)
  4. It works until I need to remove systemAssigned identity from systemAssigned + userAssigned identities, this is because passing userAssigned as empty list (because I am not removing any userAssigned identity) and AAZIdentityObject will be converting the identity to None instead of UserAssigned, cause the removal of systemAssigned + userAssigned identities.
  5. The workaround is, I have added a placeholder into the userAssigned identity list and send to customized aaz in this format {'resource_group': 'william-rg', 'vm_name': 'william-vm1', 'mi_user_assigned': ['UserAssigned']}. This way, AAZIdentityObject will set the type of identity to UserAssigned, hence retain the userAssigned identities.
  6. Then I remove the placeholder afterwards in the inherited function.

Result:
image


return json.dumps(content)

def __call__(self, *args, **kwargs):
Copy link
Copy Markdown
Contributor

@yanzhudd yanzhudd Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to manually write this __call__() function here, as I remember it is already defined in the parent class

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the call function exists but is it not workable for 'removing identity using vm patch class'. Reason as:

This is _update.py, it does support pre_instance_update, which modifies the content of request before calling the update API.
image

This is _patch.py, it does not support pre_instance_update, and I do not find any other function to perform the same function as pre_instance_update
image

And that is why I have created __call__ function and added one function to modify the content
image

@yanzhudd yanzhudd changed the title [Compute] vm identity command migration {Compute} az vm identity: Migrate commands to aaz-based implementation Dec 31, 2025
@yanzhudd
Copy link
Copy Markdown
Contributor

since the changes are not visible to users, the title should be started with {Compute} instead of [Compute]
for more information, please refer to https://github.com/Azure/azure-cli/tree/dev/doc/authoring_command_modules#submitting-pull-requests

@yanzhudd
Copy link
Copy Markdown
Contributor

could you please double check the changes to the recording yaml files? We need to ensure there is no difference for the command behavior after migrating to aaz-based

@yanzhudd yanzhudd merged commit ed8cb74 into Azure:dev Jan 12, 2026
48 checks passed
@william051200 william051200 deleted the vm-identity-migration branch January 12, 2026 06:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Auto-Assign Auto assign by bot Compute az vm/vmss/image/disk/snapshot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants