top of page
7 hours ago
11 min read

Azure Windows VM Rescue Deep Dive

Recover a broken Windows Server VM without RDP

A VM can report Running while its application and remote access are unavailable. This lab follows two recovery paths: repair an RDP failure through the guest agent, then repair a copied OS disk when that agent is unavailable. [1]

The result: the original VM, IIS page, and test file work again—without using RDP to perform the repairs.

Disposable lab only. These scripts intentionally disable Windows services. Do not run them on an existing or production VM.

Lab setting

Configuration

Workstation

Windows, PowerShell 7.4+, current Azure CLI

Azure

West Europe; quota for two Standard_D2s_v5 VMs

Workload

Windows Server 2022 Gen2, IIS, managed OS disk

Access

Workload HTTP/RDP restricted to your public IPv4; private repair VM

Scope

Standalone VM; no guest BitLocker, Azure Disk Encryption, or ephemeral OS disk

Before starting: use a lab subscription with permission to create resource groups, networks, VMs, disks, and snapshots. Keep the same PowerShell session throughout. Both VMs use Standard security for this exercise; do not downgrade an existing VM. VMs, disks, a snapshot, public IPs, and NAT Gateway incur charges until cleaned up.

1. Prepare the lab

Set the subscription and your actual public IPv4 address. Resource groups receive a unique lab suffix. The two small helpers run guest scripts and wait for agent readiness; Run Command still requires a working agent. [2]

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
if (-not $IsWindows -or $PSVersionTable.PSVersion -lt [version]'7.4') {
    throw 'Use PowerShell 7.4+ on a Windows workstation.'
}
az login --output none
$Sub = Read-Host 'Lab subscription ID'
az account set --subscription $Sub
az extension add --name vm-repair --upgrade --output none
foreach ($p in 'Microsoft.Compute','Microsoft.Network','Microsoft.Storage') {
    az provider register --namespace $p --wait --output none
}

$Id = [guid]::NewGuid().ToString('N')
$Rg = 'rg-vm-rescue-' + $Id.Substring(0,6)
$RepairRg = "$Rg-repair"
$Location = 'westeurope'
$Vm = 'vm-rescue-01'
$RepairVm = 'vm-rescue-fix'
$RepairDisk = 'osdisk-rescue-fixed'
$Size = 'Standard_D2s_v5'
$Hash = ''; $PageHash = ''
$Work = Join-Path $HOME $Rg
New-Item $Work -ItemType Directory | Out-Null
$Ip = (Read-Host 'Your public IPv4 address, without /32').Trim()
$ParsedIp = [Net.IPAddress]::Any
if (-not [Net.IPAddress]::TryParse($Ip,[ref]$ParsedIp) -or
    $ParsedIp.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
    throw 'Enter a valid IPv4 address.'
}
foreach ($g in $Rg,$RepairRg) {
    if ((az group exists --name $g -o tsv) -eq 'true') {
        throw "Resource group already exists: $g"
    }
}

function Guest([string]$Script, [string]$Marker) {
    $Script = '$ErrorActionPreference = "Stop"' + "`n" + $Script
    $Script = $Script.Replace('__ID__',$Id).Replace('__HASH__',$Hash)
    $Script = $Script.Replace('__PAGEHASH__',$PageHash)
    $File = Join-Path $Work 'guest.ps1'
    [IO.File]::WriteAllText($File,$Script,[Text.UTF8Encoding]::new($false))
    $r = az vm run-command invoke -g $Rg -n $Vm `
        --command-id RunPowerShellScript --scripts "@$File" -o json |
        ConvertFrom-Json
    $Text = ($r.value | ForEach-Object { $_.message }) -join "`n"
    if (-not $Text.Contains($Marker)) { throw $Text }
    $Text
}
function Wait-Agent([string]$Group = $Rg, [string]$Name = $Vm) {
    $Until = (Get-Date).AddMinutes(15)
    do {
        $s = az vm get-instance-view -g $Group -n $Name `
            --query 'vmAgent.statuses[].code' -o tsv
        if ($s -contains 'ProvisioningState/succeeded') { return }
        Start-Sleep 10
    } while ((Get-Date) -lt $Until)
    throw 'Agent not ready. Stop and inspect the VM and outbound connectivity.'
}

2. Deploy Windows Server

Create the network, restrict inbound access to your /32, and deploy the VM. Its attached Standard public IP supplies explicit outbound access; the subnet does not rely on default outbound access. [3]

az group create -n $Rg -l $Location --tags "labId=$Id" -o none
az network vnet create -g $Rg -n vnet-rescue -l $Location `
    --address-prefixes 10.80.0.0/16 --subnet-name workload `
    --subnet-prefixes 10.80.1.0/24 -o none
az network vnet subnet update -g $Rg --vnet-name vnet-rescue `
    -n workload --default-outbound false -o none
az network nsg create -g $Rg -n nsg-rescue -l $Location -o none
az network nsg rule create -g $Rg --nsg-name nsg-rescue `
    -n Allow-Operator --priority 100 --access Allow --protocol Tcp `
    --direction Inbound --source-address-prefixes "$Ip/32" `
    --source-port-ranges '*' --destination-address-prefixes '*' `
    --destination-port-ranges 80 3389 -o none
az network nsg rule create -g $Rg --nsg-name nsg-rescue `
    -n Deny-Other-Inbound --priority 200 --access Deny --protocol '*' `
    --direction Inbound --source-address-prefixes '*' `
    --source-port-ranges '*' --destination-address-prefixes '*' `
    --destination-port-ranges '*' -o none
az network public-ip create -g $Rg -n pip-rescue -l $Location `
    --sku Standard --allocation-method Static -o none
az network nic create -g $Rg -n nic-rescue -l $Location `
    --vnet-name vnet-rescue --subnet workload `
    --network-security-group nsg-rescue --public-ip-address pip-rescue -o none

$Credential = Get-Credential -UserName rescueadmin -Message 'Lab VM password'
$Password = $Credential.GetNetworkCredential().Password
try {
    az vm create -g $Rg -n $Vm -l $Location --size $Size `
        --image MicrosoftWindowsServer:WindowsServer:2022-datacenter-g2:latest `
        --security-type Standard --admin-username rescueadmin `
        --admin-password $Password --nics nic-rescue `
        --storage-sku StandardSSD_LRS --os-disk-delete-option Detach `
        --tags "labId=$Id" -o none
} finally { $Password = $null }
az vm boot-diagnostics enable -g $Rg -n $Vm -o none
$Original = az vm show -g $Rg -n $Vm -o json | ConvertFrom-Json
$OriginalDisk = $Original.storageProfile.osDisk.managedDisk.id
$PublicIp = az network public-ip show -g $Rg -n pip-rescue `
    --query ipAddress -o tsv
Wait-Agent

3. Create and protect the baseline

Install IIS, create two identifiable files, and record their SHA-256 hashes outside the VM. All four lab services are deliberately set to Automatic; that is the known setting restored later, not a universal service configuration.

$Baseline = Guest -Marker BASELINE_OK -Script @'
if (Test-Path C:\RescueLab) { throw 'Baseline already exists.' }
$Install = Install-WindowsFeature Web-Server -IncludeManagementTools
if (-not $Install.Success -or [string]$Install.RestartNeeded -eq 'Yes') {
    throw 'IIS setup incomplete. Resolve installation or restart before continuing.'
}
New-Item C:\RescueLab -ItemType Directory | Out-Null
Set-Content C:\RescueLab\lab-id.txt '__ID__'
Set-Content C:\RescueLab\payload.txt 'Preserve this original file: __ID__'
Set-Content C:\inetpub\wwwroot\index.html '<h1>Azure VM Rescue</h1><p>__ID__</p>'
foreach ($Name in 'TermService','WindowsAzureGuestAgent','RdAgent','W3SVC') {
    Set-Service $Name -StartupType Automatic
    Start-Service $Name
}
$Rdp = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
Set-ItemProperty $Rdp fDenyTSConnections 0
Set-ItemProperty "$Rdp\WinStations\RDP-Tcp" UserAuthentication 1
Enable-NetFirewallRule -Name RemoteDesktop-UserMode-In-TCP
New-NetFirewallRule -Name RescueHTTP -DisplayName RescueHTTP `
    -Direction Inbound -Action Allow -Protocol TCP -LocalPort 80 | Out-Null
"FILE_HASH=$((Get-FileHash C:\RescueLab\payload.txt).Hash)"
"PAGE_HASH=$((Get-FileHash C:\inetpub\wwwroot\index.html).Hash)"
'BASELINE_OK'
'@
$Hash = [regex]::Match($Baseline,'FILE_HASH=([A-F0-9]{64})').Groups[1].Value
$PageHash = [regex]::Match($Baseline,'PAGE_HASH=([A-F0-9]{64})').Groups[1].Value
if ($Hash.Length -ne 64 -or $PageHash.Length -ne 64) { throw 'Missing hashes.' }
$Baseline | Set-Content (Join-Path $Work 'baseline.txt')
$Page = Invoke-WebRequest "http://$PublicIp/index.html" -NoProxy -TimeoutSec 15
if (-not $Page.Content.Contains($Id)) { throw 'HTTP baseline failed.' }
if (-not (Test-NetConnection $PublicIp -Port 3389 -InformationLevel Quiet)) {
    throw 'RDP baseline failed.'
}

Take a known-good snapshot while the VM is deallocated, then start it again. Keep this snapshot until the exercise has passed validation. [4]

az vm deallocate -g $Rg -n $Vm -o none
az snapshot create -g $Rg -n snap-rescue-baseline -l $Location `
    --source $OriginalDisk --sku Standard_LRS -o none
az vm start -g $Rg -n $Vm -o none
Wait-Agent
mstsc.exe "/v:$PublicIp"

Sign in with rescueadmin, confirm access, then sign out. Do not inject a fault until HTTP and RDP both work. Passwords are passed to CLI processes during VM creation; do not run credential logging on a shared workstation.

4. Break and recover RDP

Disable Remote Desktop while leaving IIS and the guest agent running. The repair uses Run Command—not an RDP session. [2]

if ((Read-Host 'Type BREAK-A to disable lab RDP') -ne 'BREAK-A') { throw 'Cancelled.' }
Guest -Marker FAULT_A_OK -Script @'
if ((Get-Content C:\RescueLab\lab-id.txt -Raw).Trim() -ne '__ID__') {
    throw 'Wrong lab target.'
}
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
    fDenyTSConnections 1
Set-Service TermService -StartupType Disabled
Stop-Service TermService -Force
'FAULT_A_OK'
'@
Test-NetConnection $PublicIp -Port 3389 -InformationLevel Quiet
(Invoke-WebRequest "http://$PublicIp/index.html" -NoProxy -TimeoutSec 15).StatusCode

Expected: the TCP test returns False, but HTTP returns 200. Restore the recorded lab settings:

Guest -Marker ONLINE_REPAIR_OK -Script @'
if ((Get-Content C:\RescueLab\lab-id.txt -Raw).Trim() -ne '__ID__') {
    throw 'Wrong lab target.'
}
Set-Service TermService -StartupType Automatic
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
    fDenyTSConnections 0
Start-Service TermService
'ONLINE_REPAIR_OK'
'@
if (-not (Test-NetConnection $PublicIp -Port 3389 -InformationLevel Quiet)) {
    throw 'RDP has not recovered.'
}

5. Make agent-based management unavailable

This time disable startup for RDP, IIS, and both Azure agent services. Do not stop the agent inside its own Run Command. Let the command finish, then restart from the workstation. [2]

if ((Read-Host 'Type BREAK-B to disable lab services') -ne 'BREAK-B') { throw 'Cancelled.' }
Guest -Marker FAULT_B_ARMED -Script @'
if ((Get-Content C:\RescueLab\lab-id.txt -Raw).Trim() -ne '__ID__') {
    throw 'Wrong lab target.'
}
foreach ($Name in 'TermService','WindowsAzureGuestAgent','RdAgent','W3SVC') {
    Set-Service $Name -StartupType Disabled
}
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
    fDenyTSConnections 1
'FAULT_B_ARMED'
'@
az vm restart -g $Rg -n $Vm -o none
az vm get-instance-view -g $Rg -n $Vm `
    --query '{Power:statuses,Agent:vmAgent}' -o json
Test-NetConnection $PublicIp -Port 3389 -InformationLevel Quiet

After the restart, RDP and HTTP should be unavailable. Windows itself can still boot: this is a service-configuration failure, not bootloader damage. Agent status can lag; do not queue another Run Command against the deliberately disabled agent.

6. Create the private repair VM

Deallocate the source and keep it stopped until restore. repair create attaches a copy of its OS disk to a separate VM. Omitting --associate-public-ip keeps that repair VM private. [1]

az vm deallocate -g $Rg -n $Vm -o none
$RepairPassword = 'Az9' + [guid]::NewGuid().ToString('N') + 'qR7'
try {
    $r = az vm repair create -g $Rg -n $Vm `
        --repair-group-name $RepairRg --repair-vm-name $RepairVm `
        --repair-username repairadmin --repair-password $RepairPassword `
        --copy-disk-name $RepairDisk --size $Size `
        --os-disk-type StandardSSD_LRS --disable-trusted-launch -o json |
        ConvertFrom-Json
} finally { $RepairPassword = $null }
if ($r.status -ne 'SUCCESS') { throw ($r | ConvertTo-Json -Depth 10) }
az group update -n $RepairRg --set "tags.labId=$Id" -o none
$Repair = az vm show -g $RepairRg -n $RepairVm -o json | ConvertFrom-Json
$RepairId = $Repair.id
$RepairNicId = $Repair.networkProfile.networkInterfaces[0].id
$Nic = az network nic show --ids $RepairNicId -o json | ConvertFrom-Json
$SubnetId = $Nic.ipConfigurations[0].subnet.id
$RepairPublicIp = az network nic show --ids $RepairNicId `
    --query 'ipConfigurations[0].publicIPAddress.id' -o tsv
if ($RepairPublicIp) { throw 'Repair NIC unexpectedly has a public IP.' }
if (@($Repair.storageProfile.dataDisks).Count -ne 1 -or
    $Repair.storageProfile.dataDisks[0].name -ne $RepairDisk) {
    throw 'Expected exactly one copied OS disk.'
}
$CopyId = $Repair.storageProfile.dataDisks[0].managedDisk.id
if ($CopyId -eq $OriginalDisk) { throw 'Refusing to repair the original disk.' }

Attach NAT Gateway for outbound access and deny inbound traffic on the repair NIC. NAT is outbound-only; it does not expose the repair VM. The repair driver also needs outbound access to its GitHub script library. [5]

az network nsg create -g $RepairRg -n nsg-repair -l $Location -o none
az network nsg rule create -g $RepairRg --nsg-name nsg-repair `
    -n Deny-Inbound --priority 100 --access Deny --protocol '*' `
    --direction Inbound --source-address-prefixes '*' `
    --source-port-ranges '*' --destination-address-prefixes '*' `
    --destination-port-ranges '*' -o none
$NsgId = az network nsg show -g $RepairRg -n nsg-repair --query id -o tsv
az network nic update --ids $RepairNicId --network-security-group $NsgId -o none
$NatIp = az network public-ip create -g $RepairRg -n pip-nat -l $Location `
    --sku Standard --allocation-method Static --query publicIp.id -o tsv
$NatId = az network nat gateway create -g $RepairRg -n nat-repair `
    -l $Location --public-ip-addresses $NatIp --query id -o tsv
az network vnet subnet update --ids $SubnetId `
    --nat-gateway $NatId --default-outbound false -o none
Wait-Agent $RepairRg $RepairVm

7. Repair the copied Windows installation

The script identifies the copied Windows volume using the lab marker and original file hashes, then loads its SYSTEM hive. It restores the lab's four services to Automatic and enables RDP without disabling NLA. The current and default control sets are read from Select; no drive letter or control-set number is assumed. [7]

Do not initialize, format, or guess a disk. This script is only for the fresh, single-data-disk repair VM created above. Stop if its identity checks fail.
$Offline = @'
$ErrorActionPreference = 'Stop'
$Hive = $null; $Loaded = $false
try {
    $Disks = @(Get-Disk | Where-Object { -not $_.IsBoot -and -not $_.IsSystem })
    if ($Disks.Count -ne 1) { throw 'Expected one non-system disk.' }
    $Disk = $Disks[0]
    if ([string]$Disk.PartitionStyle -ne 'GPT') { throw 'Expected a GPT disk.' }
    Set-Disk -Number $Disk.Number -IsOffline $false
    Set-Disk -Number $Disk.Number -IsReadOnly $false
    $Roots = @()
    foreach ($p in (Get-Partition -DiskNumber $Disk.Number)) {
        if (([string]$p.GptType).Trim('{}') -ne
            'ebd0a0a2-b9e5-4433-87c0-68b6b72699c7') { continue }
        if (-not $p.DriveLetter) {
            $p | Add-PartitionAccessPath -AssignDriveLetter
            $p = Get-Partition -DiskNumber $Disk.Number -PartitionNumber $p.PartitionNumber
        }
        if (-not $p.DriveLetter) { continue }
        $Root = "$($p.DriveLetter):\"
        $Marker = Join-Path $Root 'RescueLab\lab-id.txt'
        if ((Test-Path $Marker) -and
            (Get-Content $Marker -Raw).Trim() -eq '__ID__') { $Roots += $Root }
    }
    if ($Roots.Count -ne 1) { throw 'Copied Windows volume not uniquely identified.' }
    $Root = $Roots[0]
    if ($Root.TrimEnd('\') -ieq $env:SystemDrive) { throw 'Refusing system drive.' }
    if ((Get-FileHash "${Root}RescueLab\payload.txt").Hash -ne '__HASH__' -or
        (Get-FileHash "${Root}inetpub\wwwroot\index.html").Hash -ne '__PAGEHASH__') {
        throw 'Copied file hashes do not match the external baseline.'
    }
    if (Test-Path 'Registry::HKEY_LOCAL_MACHINE\RescueOffline') { throw 'Hive already loaded.' }
    & reg.exe load HKLM\RescueOffline "${Root}Windows\System32\config\SYSTEM" | Out-Null
    if ($LASTEXITCODE -ne 0) { throw 'Could not load SYSTEM hive.' }
    $Loaded = $true
    $Hive = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey('RescueOffline',$true)
    if ($null -eq $Hive) { throw 'Cannot open loaded hive.' }
    $Select = $Hive.OpenSubKey('Select')
    if ($null -eq $Select) { throw 'Select key missing.' }
    try {
        $Numbers = @([int]$Select.GetValue('Current'),[int]$Select.GetValue('Default')) |
            Sort-Object -Unique
    } finally { $Select.Dispose() }

    function Set-OfflineValue($Path,$Name,[int]$Value) {
        $Key = $Hive.OpenSubKey($Path,$true)
        if ($null -eq $Key) { throw "Missing registry key: $Path" }
        try {
            $Key.SetValue($Name,$Value,[Microsoft.Win32.RegistryValueKind]::DWord)
            if ([int]$Key.GetValue($Name) -ne $Value) { throw 'Registry verification failed.' }
        } finally { $Key.Dispose() }
    }
    foreach ($n in $Numbers) {
        if ($n -lt 1) { throw 'Invalid control set.' }
        $cs = 'ControlSet{0:D3}' -f $n
        foreach ($Name in 'TermService','WindowsAzureGuestAgent','RdAgent','W3SVC') {
            Set-OfflineValue "$cs\Services\$Name" Start 2
        }
        $Rdp = "$cs\Control\Terminal Server"
        Set-OfflineValue $Rdp fDenyTSConnections 0
        Set-OfflineValue "$Rdp\WinStations\RDP-Tcp" UserAuthentication 1
    }
    $Hive.Flush()
} finally {
    if ($null -ne $Hive) { $Hive.Dispose() }
    if ($Loaded) {
        [GC]::Collect(); [GC]::WaitForPendingFinalizers()
        & reg.exe unload HKLM\RescueOffline | Out-Null
        if ($LASTEXITCODE -ne 0) { throw 'Hive not unloaded. Do not restore.' }
    }
}
Set-Disk -Number $Disk.Number -IsOffline $true
$Stamp = [datetime]::UtcNow.ToString('MM/dd/yyyy HH:mm:ss',
    [Globalization.CultureInfo]::InvariantCulture)
"[Output $Stamp] OFFLINE_REPAIR_OK"
'[STATUS]::SUCCESS'
'@
$Offline = $Offline.Replace('__ID__',$Id).Replace('__HASH__',$Hash)
$Offline = $Offline.Replace('__PAGEHASH__',$PageHash)
$ScriptPath = Join-Path $Work 'offline-repair.ps1'
[IO.File]::WriteAllText($ScriptPath,$Offline,[Text.UTF8Encoding]::new($false))

$r = az vm repair run -g $Rg -n $Vm --repair-vm-id $RepairId `
    --run-on-repair --custom-script-file $ScriptPath -o json | ConvertFrom-Json
$r | ConvertTo-Json -Depth 20 | Set-Content (Join-Path $Work 'repair-result.json')
if ($r.status -ne 'SUCCESS' -or $r.script_status -ne 'SUCCESS' -or
    [string]$r.logs -notmatch 'OFFLINE_REPAIR_OK') {
    throw 'Offline repair failed. Do not restore the disk.'
}

Only proceed when both the command and script report success. The extension distinguishes the outer command result from the repair script result. [6]

8. Restore and validate

Deallocate the repair VM, swap the repaired disk into the original VM, and start it. Keep the repair resources until validation is complete. [1]

az vm deallocate -g $RepairRg -n $RepairVm -o none
$State = az vm get-instance-view -g $Rg -n $Vm `
    --query "statuses[?starts_with(code, 'PowerState/')].code | [0]" -o tsv
if ($State -ne 'PowerState/deallocated') { throw 'Source must remain deallocated.' }
$r = az vm repair restore -g $Rg -n $Vm --repair-vm-id $RepairId `
    --disk-name $RepairDisk --no-cleanup -o json | ConvertFrom-Json
if ($r.status -ne 'SUCCESS') { throw ($r | ConvertTo-Json -Depth 10) }
$Current = az vm show -g $Rg -n $Vm -o json | ConvertFrom-Json
if ($Current.id -ne $Original.id -or
    $Current.networkProfile.networkInterfaces[0].id -ne
        $Original.networkProfile.networkInterfaces[0].id -or
    $Current.storageProfile.osDisk.managedDisk.id -ne $CopyId) {
    throw 'Unexpected VM, NIC, or OS-disk association.'
}
az vm start -g $Rg -n $Vm -o none
Wait-Agent

Verify the original files and services. Do not rerun setup or recreate the page to make validation pass.

Guest -Marker RECOVERY_OK -Script @'
if ((Get-Content C:\RescueLab\lab-id.txt -Raw).Trim() -ne '__ID__') {
    throw 'Wrong lab target.'
}
if ((Get-FileHash C:\RescueLab\payload.txt).Hash -ne '__HASH__' -or
    (Get-FileHash C:\inetpub\wwwroot\index.html).Hash -ne '__PAGEHASH__') {
    throw 'Original file integrity failed.'
}
foreach ($Name in 'TermService','WindowsAzureGuestAgent','RdAgent','W3SVC') {
    $Start = Get-ItemPropertyValue "HKLM:\SYSTEM\CurrentControlSet\Services\$Name" Start
    if ((Get-Service $Name).Status -ne 'Running' -or $Start -ne 2) {
        throw "Service not recovered: $Name"
    }
}
$Rdp = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
if ((Get-ItemPropertyValue $Rdp fDenyTSConnections) -ne 0 -or
    (Get-ItemPropertyValue "$Rdp\WinStations\RDP-Tcp" UserAuthentication) -ne 1) {
    throw 'RDP or NLA settings incorrect.'
}
'RECOVERY_OK'
'@
$Page = Invoke-WebRequest "http://$PublicIp/index.html" -NoProxy -TimeoutSec 15
if (-not $Page.Content.Contains($Id)) { throw 'Application test failed.' }
if (-not (Test-NetConnection $PublicIp -Port 3389 -InformationLevel Quiet)) {
    throw 'RDP transport test failed.'
}
mstsc.exe "/v:$PublicIp"

Sign in using the original credentials. Passing TCP 3389 alone does not prove authentication; this final sign-in completes the access check. The original failed disk and known-good snapshot remain available until cleanup.

9. Clean up

This deletes both dedicated lab groups, including VMs, copied and original disks, the snapshot, and NAT resources. The local baseline and repair-result files remain in $Work.

if ((az account show --query id -o tsv) -ne $Sub) { throw 'Subscription changed.' }
foreach ($g in $Rg,$RepairRg) {
    if ((az group exists -n $g -o tsv) -eq 'true') {
        if ((az group show -n $g --query tags.labId -o tsv) -ne $Id) {
            throw "Lab tag mismatch: $g"
        }
        az resource list -g $g --query '[].{Name:name,Type:type}' -o table
    }
}
if ((Read-Host "Type $Id to delete both lab groups") -ne $Id) { throw 'Cancelled.' }
# Delete the source VM before the group containing its repaired disk.
foreach ($g in $Rg,$RepairRg) {
    if ((az group exists -n $g -o tsv) -eq 'true') {
        az group delete -n $g --yes
    }
    if ((az group exists -n $g -o tsv) -ne 'false') { throw "Cleanup incomplete: $g" }
}
$Credential = $null

Workshop complete. You recovered RDP through the agent, then recovered an agent-unavailable VM through offline disk repair. The success criteria were unchanged files, restored services, a working application, and normal sign-in—not just a running VM.

Microsoft references

 
 
 

Comments


bottom of page