Change-VMZone.ps1 (164 lines of code) (raw):

param ( [Parameter(Mandatory=$true)] [string]$subscriptionId, [Parameter(Mandatory=$true)] [string]$resourceGroupName, [Parameter(Mandatory=$true)] [string]$vmName, [Parameter(Mandatory=$true)] [string]$targetZone, [Parameter(Mandatory=$false)] [string]$newNetworkResourceId ) $ErrorActionPreference = "Stop" function UpdateZone { param ( [Parameter(Mandatory=$true)] [string]$subscriptionId, [Parameter(Mandatory=$true)] [string]$resourceGroupName, [Parameter(Mandatory=$true)] [string]$vmName, [Parameter(Mandatory=$true)] [string]$targetZone, [Parameter(Mandatory=$true)] [string]$token, [string]$newNetworkProfile ) $uri = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Compute/virtualMachines/$vmName`?api-version=2024-07-01" $headers = @{ "Authorization" = "Bearer $token" "Content-Type" = "application/json" } $body = [PSCustomObject]@{ "zones" = @($targetZone) } if ($null -ne $newNetworkProfile -and -not [string]::IsNullOrEmpty($newNetworkProfile)) { $networkProfileObject = $newNetworkProfile | ConvertFrom-Json $properties = [PSCustomObject]@{ "networkProfile" = $networkProfileObject } $body | Add-Member -MemberType NoteProperty -Name "properties" -Value $properties } $body = $body | ConvertTo-Json -Depth 10 Write-Output "request payload: $body" InvokeAzureApiAndPollForCompletion -uri $uri -method "PATCH" -headers $headers -payload $body } function ForceDeallocate { param ( [Parameter(Mandatory=$true)] [string]$subscriptionId, [Parameter(Mandatory=$true)] [string]$resourceGroupName, [Parameter(Mandatory=$true)] [string]$vmName, [Parameter(Mandatory=$true)] [string]$token ) $uri = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Compute/virtualMachines/$vmName/deallocate`?api-version=2024-07-01" $headers = @{ "Authorization" = "Bearer $token" "Content-Type" = "application/json" } InvokeAzureApiAndPollForCompletion -uri $uri -method "POST" -headers $headers } function StartVM { param ( [Parameter(Mandatory=$true)] [string]$subscriptionId, [Parameter(Mandatory=$true)] [string]$resourceGroupName, [Parameter(Mandatory=$true)] [string]$vmName, [Parameter(Mandatory=$true)] [string]$token ) $uri = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Compute/virtualMachines/$vmName/start`?api-version=2024-07-01" $headers = @{ "Authorization" = "Bearer $token" "Content-Type" = "application/json" } InvokeAzureApiAndPollForCompletion -uri $uri -method "POST" -headers $headers } function PollForCompletion { param ( [Parameter(Mandatory=$true)] [string]$asyncOperationUrl, [Parameter(Mandatory=$true)] [string]$token ) $headers = @{ "Authorization" = "Bearer $token" "Content-Type" = "application/json" } $counter = 0 $upperCircuit = 400 while ($counter -lt $upperCircuit) { $response = Invoke-WebRequest -Uri $asyncOperationUrl -Headers $headers $statusCode = $response.StatusCode if ($statusCode -eq 200) { $responsePayload = $response.Content | ConvertFrom-Json $operationStatus = $responsePayload.status Write-Output "Operation status $operationStatus" if ($operationStatus -eq "Succeeded") { break } Write-Output "Waiting 20 seconds" Start-Sleep -Seconds 20 } else { throw "GET operation failed with $statusCode" } $counter++ } if ($counter -eq $upperCircuit) { throw "Operation timed out" } } function InvokeAzureApiAndPollForCompletion { param ( [Parameter(Mandatory=$true)] [string]$uri, [Parameter(Mandatory=$true)] [string]$method, [Parameter(Mandatory=$true)] [object]$headers, [object]$payload ) Write-Output "Invoking $method : $uri" $response = Invoke-WebRequest -Uri $uri -Method $method -Headers $headers -Body $body $statusCode = $response.StatusCode if ($statusCode -eq 200 -or $statusCode -eq 202) { $asyncHeader = $response.Headers["azure-asyncOperation"] Write-Output "Status Code: $statusCode" if ($null -ne $asyncHeader) { $asyncOperationUrl = "$asyncHeader" Write-Output "Polling for operation completion. Async header: $asyncHeader." PollForCompletion -asyncOperationUrl $asyncOperationUrl -token $token } } else { throw "azure api $method : $uri failed with $statusCode" } } #TODO 1: Only continue execution if zone or network profile actually needs to be changed if ($newNetworkResourceId) { $networkProfile = @{ "networkInterfaces" = @( @{ "id" = $newNetworkResourceId "properties" = @{ "deleteOption" = "Detach" } } ) } $networkProfile = $networkProfile | ConvertTo-Json -Depth 5 } else {$networkProfile = $null} Connect-AzAccount -SubscriptionId $subscriptionId $token = (Get-AzAccessToken).Token Write-Output "Stopping the VM." ForceDeallocate -subscriptionId $subscriptionId -resourceGroupName $resourceGroupName -vmName $vmName -token $token Write-Output "Updating zone" UpdateZone -subscriptionId $subscriptionId -resourceGroupName $resourceGroupName -vmName $vmName -targetZone $targetZone -token $token -newNetworkProfile $networkProfile Write-Output "Starting the VM" StartVM -subscriptionId $subscriptionId -resourceGroupName $resourceGroupName -vmName $vmName -token $token