Fix Remove-rsv.ps1 script#221
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request fixes issues with the Remove-rsv.ps1 PowerShell script that removes Azure Recovery Services Vaults, along with related improvements to Bicep templates and other scripts.
Changes:
- Updated Recovery Services Vault API version to 2025-08-01 and added async operation polling logic
- Commented out code sections in
Remove-rsv.ps1that no longer work due to Azure policy changes (soft delete cannot be disabled, ASR operations fail) - Improved error handling in
Remove-Spoke.ps1with better resource existence checks and exit codes - Fixed variable naming consistency from
{rgname}to{rgName}in Bicep templates - Added safe navigation operators for conditional module outputs in Bicep
- Removed temporary Az.Resources version restriction from deployment scripts
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/PowerShell/Scripts/Recovery/Remove-rsv.ps1 | Major refactoring: commented out soft delete and ASR cleanup code that no longer works, added async operation polling logic for vault deletion, updated API version |
| scripts/PowerShell/Scripts/Remove-Spoke.ps1 | Improved error handling with resource existence checks, better warning messages, and exit code on errors |
| shared-modules/recovery/recoveryServicesVault.bicep | Updated vault API version to 2025-08-01, fixed deployment name length to 64 characters, added comment about future user-assigned identity support |
| research-spoke/main.bicep | Fixed variable naming from {rgname} to {rgName}, added safe navigation operators for conditional modules, added BCP334 linter suppressions, commented out unused import |
| research-spoke/deploy.ps1 | Removed temporary Az.Resources version restriction |
| research-hub/deploy.ps1 | Removed temporary Az.Resources version restriction |
| .vscode/settings.json | Added new words to spell checker dictionary |
| .vscode/launch.json | Added PowerShell debug configuration for Remove-rsv.ps1 script |
| .gitignore | Added AzPortalRefScript folder to ignore list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| # Derive AzRestMethod path from full URL; need to remove 'https://management.azure.com' prefix but not the `/` | ||
| # https://learn.microsoft.com/powershell/module/az.accounts/invoke-azrestmethod?view=azps-15.1.0#-path | ||
| $AzRestMethodPath = $OperationUrl -replace "^$((Get-AzContext).Environment.ResourceManagerUrl)", "/" |
There was a problem hiding this comment.
The regex replacement on line 277 removes the ResourceManagerUrl but then adds back a leading slash. However, if the ResourceManagerUrl from Get-AzContext doesn't end with a slash but the OperationUrl doesn't have a slash after the domain, this could result in '//' or missing path. Consider verifying the regex pattern handles all cases correctly, or use a more explicit string manipulation approach like: '$OperationUrl.Substring((Get-AzContext).Environment.ResourceManagerUrl.Length)'
| $AzRestMethodPath = $OperationUrl -replace "^$((Get-AzContext).Environment.ResourceManagerUrl)", "/" | |
| $ctx = Get-AzContext | |
| $resourceManagerUrl = $ctx.Environment.ResourceManagerUrl.TrimEnd('/') | |
| if ($OperationUrl.StartsWith($resourceManagerUrl, [System.StringComparison]::OrdinalIgnoreCase)) { | |
| $AzRestMethodPath = $OperationUrl.Substring($resourceManagerUrl.Length) | |
| } | |
| else { | |
| # Fallback: if the OperationUrl does not start with the expected ResourceManagerUrl, use it as-is | |
| $AzRestMethodPath = $OperationUrl | |
| } | |
| # Ensure AzRestMethod path starts with a single leading slash | |
| if (-not $AzRestMethodPath.StartsWith('/')) { | |
| $AzRestMethodPath = '/' + $AzRestMethodPath | |
| } |
|
|
||
| #Write-Host "Warning: This script will only remove the replication configuration from Azure Site Recovery and not from the source. Please cleanup the source manually. Visit https://go.microsoft.com/fwlink/?linkid=2182781 to learn more." -ForegroundColor Yellow | ||
|
|
||
| foreach ($item in $pvtEndpoints) { |
There was a problem hiding this comment.
Variable naming inconsistency: The variable is defined as '$pvtendpoints' (lowercase) on line 75, but referenced as '$pvtEndpoints' (camelCase) on line 177. PowerShell is case-insensitive so this will work, but it's inconsistent. The variable should use the same casing throughout for better code maintainability.
| foreach ($item in $pvtEndpoints) { | |
| foreach ($item in $pvtendpoints) { |
There was a problem hiding this comment.
Suggest a fix on line 75 instead as line 177 uses the correct casing
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| # [int]$PollIntervalSec = 0 # base interval | ||
| # if ($headers.Contains("Retry-After")) { | ||
| [int]$PollIntervalSec = [int]${headers.GetValues("Retry-After")}?[0] ?? 20 | ||
| # } | ||
| # else { | ||
| # $PollIntervalSec = 20 | ||
| # } |
| $opResponse = Invoke-AzRestMethod -Method GET -Path $AzRestMethodPath -ErrorAction SilentlyContinue | ||
|
|
| Write-Host "`n❌ An error occurred: $($_)" | ||
| Write-Host $_.ScriptStackTrace | ||
| Write-Host "In context $AzContext" | ||
| Write-Host "In subscription: $($AzContext.Subscription.Id) - $($AzContext.Subscription.Name)" |
| # HACK: 2025-11-16: svaelter: Can no longer disable soft delete in any region for a Recovery Services Vault. | ||
| # Ref: https://learn.microsoft.com/azure/backup/secure-by-default?tabs=preview#supported-scenarios | ||
| #Set-AzRecoveryServicesVaultProperty -Vault $VaultToDelete.ID -SoftDeleteFeatureState Disable #disable soft delete |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
scripts/PowerShell/Scripts/Recovery/Remove-rsv.ps1:181
- Variable casing is inconsistent:
$pvtendpointsis declared earlier, but the loop uses$pvtEndpoints. PowerShell is case-insensitive so it works, but it reduces readability and makes it easier to miss typos. Consider renaming the declaration to$pvtEndpoints(or revert the loop) so the casing is consistent throughout the script.
foreach ($item in $pvtEndpoints) {
$penamesplit = $item.Name.Split(".")
$pename = $penamesplit[0]
Remove-AzPrivateEndpointConnection -ResourceId $item.Id -Force #remove private endpoint connections
Remove-AzPrivateEndpoint -Name $pename -ResourceGroupName $ResourceGroup -Force #remove private endpoints
| Select-AzSubscription $SubscriptionId -Tenant $Tenant | Out-Null | ||
| $VaultToDelete = Get-AzRecoveryServicesVault -Name $VaultName -ResourceGroupName $ResourceGroup | ||
| Write-Verbose "Selected Recovery Services Vault: $($VaultToDelete.Name) in Resource Group: $($VaultToDelete.ResourceGroupName)`n$($VaultToDelete.ID)" | ||
| # Ignore WhatIfPreference here because future cmdlets will fail without this being set | ||
| # This should have no side effects | ||
| Set-AzRecoveryServicesAsrVaultContext -Vault $VaultToDelete -WhatIf:$false | ||
| # Set-AzRecoveryServicesAsrVaultContext -Vault $VaultToDelete -WhatIf:$false | ||
| #Set-AzRecoveryServicesVaultContext -Vault $VaultToDelete #-WhatIf:$false | ||
|
|
||
| $UpdatedVault = Update-AzRecoveryServicesVault -ResourceGroupName $VaultToDelete.ResourceGroupName -Name $VaultToDelete.Name -ImmutabilityState "Disabled" | ||
| Write-Host "Immutability state set to $($UpdatedVault.Properties.ImmutabilitySettings.ImmutabilityState)" | ||
| Write-Host "Immutability state set to '$($UpdatedVault.Properties.ImmutabilitySettings.ImmutabilityState)'." |
| # Ignore WhatIfPreference here because future cmdlets will fail without this being set | ||
| # This should have no side effects | ||
| Set-AzRecoveryServicesAsrVaultContext -Vault $VaultToDelete -WhatIf:$false | ||
| # Set-AzRecoveryServicesAsrVaultContext -Vault $VaultToDelete -WhatIf:$false | ||
| #Set-AzRecoveryServicesVaultContext -Vault $VaultToDelete #-WhatIf:$false | ||
|
|
| # if ($ASRProtectedItems -ne 0) { Write-Host $ASRProtectedItems "ASR protected items are still present in the vault. Remove the same for successful vault deletion." -ForegroundColor Red } | ||
| # if ($ASRPolicyMappings -ne 0) { Write-Host $ASRPolicyMappings "ASR policy mappings are still present in the vault. Remove the same for successful vault deletion." -ForegroundColor Red } | ||
| # if ($fabricCount -ne 0) { Write-Host $fabricCount "ASR Fabrics are still present in the vault. Remove the same for successful vault deletion." -ForegroundColor Red } | ||
| if ($pvtendpointsFin.count -ne 0) { Write-Host $pvtendpointsFin.count "Private endpoints are still linked to the vault. Remove the same for successful vault deletion." -ForegroundColor Red } |
| # [int]$PollIntervalSec = 0 # base interval | ||
| # if ($headers.Contains("Retry-After")) { | ||
| [int]$PollIntervalSec = [int]${headers.GetValues("Retry-After")}?[0] ?? 20 | ||
| # } | ||
| # else { | ||
| # $PollIntervalSec = 20 | ||
| # } |
| while ((Get-Date) -lt $Deadline) { | ||
| $Attempt++ | ||
|
|
||
| $opResponse = Invoke-AzRestMethod -Method GET -Path $AzRestMethodPath -ErrorAction SilentlyContinue | ||
|
|
||
| # If the polling request returns 404 Not Found, it means that the resource is deleted | ||
| if (404 -eq $opResponse.StatusCode) { | ||
| # Some services return 404 Not Found when the resource is deleted | ||
| Write-Host "DELETE operation succeeded (resource not found)." | ||
| break | ||
| } | ||
|
|
||
| # Attempt to parse JSON; handle cases where content may be empty or non-JSON | ||
| $opJson = $opResponse.Content | ConvertFrom-Json -ErrorAction SilentlyContinue | ||
|
|
| "<your-resource-group>", | ||
| "-ResourceGroup", | ||
| "<your-vault-name>", | ||
| "-VaultName", | ||
| "<your-subscription-id>", | ||
| "-SubscriptionId", | ||
| "YOUR_SUBSCRIPTION_ID", |
| Write-Host "`n❌ An error occurred: $($_)" | ||
| Write-Host $_.ScriptStackTrace | ||
| Write-Host "In context $AzContext" | ||
| Write-Host "In subscription: $($AzContext.Subscription.Id) - $($AzContext.Subscription.Name)" |
…null Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
scripts/PowerShell/Scripts/Recovery/Remove-rsv.ps1:224
$pvtEndpointsFinis assigned here but referenced later as$pvtendpointsFin(different casing). PowerShell is case-insensitive so it works, but keeping a single casing throughout will reduce confusion (especially when similarly-named variables exist).
$pvtEndpointsFin = Get-AzPrivateEndpointConnection -PrivateLinkResourceId $VaultToDelete.ID
| "<your-resource-group>", | ||
| "-ResourceGroup", | ||
| "<your-vault-name>", | ||
| "-VaultName", | ||
| "<your-subscription-id>", | ||
| "-SubscriptionId", | ||
| "YOUR_SUBSCRIPTION_ID", |
| # Ignore WhatIfPreference here because future cmdlets will fail without this being set | ||
| # This should have no side effects | ||
| Set-AzRecoveryServicesAsrVaultContext -Vault $VaultToDelete -WhatIf:$false | ||
| # Set-AzRecoveryServicesAsrVaultContext -Vault $VaultToDelete -WhatIf:$false | ||
| #Set-AzRecoveryServicesVaultContext -Vault $VaultToDelete #-WhatIf:$false |
| # [int]$PollIntervalSec = 0 # base interval | ||
| # if ($headers.Contains("Retry-After")) { | ||
| [int]$PollIntervalSec = [int]${headers.GetValues("Retry-After")}?[0] ?? 20 | ||
| # } | ||
| # else { | ||
| # $PollIntervalSec = 20 | ||
| # } |
| # Derive AzRestMethod path from full URL; need to remove 'https://management.azure.com' prefix but not the `/` | ||
| # https://learn.microsoft.com/powershell/module/az.accounts/invoke-azrestmethod?view=azps-15.1.0#-path | ||
| $AzRestMethodPath = $OperationUrl -replace "^$((Get-AzContext).Environment.ResourceManagerUrl)", "/" |
| $opResponse = Invoke-AzRestMethod -Method GET -Path $AzRestMethodPath -ErrorAction SilentlyContinue | ||
|
|
|
|
||
| #Write-Host "Warning: This script will only remove the replication configuration from Azure Site Recovery and not from the source. Please cleanup the source manually. Visit https://go.microsoft.com/fwlink/?linkid=2182781 to learn more." -ForegroundColor Yellow | ||
|
|
||
| foreach ($item in $pvtEndpoints) { |
| Write-Host "`n❌ An error occurred: $($_)" | ||
| Write-Host $_.ScriptStackTrace | ||
| Write-Host "In context $AzContext" | ||
| Write-Host "In subscription: $($AzContext.Subscription.Id) - $($AzContext.Subscription.Name)" |
No description provided.