diff --git a/src/windows/sac-enabler.ps1 b/src/windows/sac-enabler.ps1 index 31e0b7d..d4e77bf 100644 --- a/src/windows/sac-enabler.ps1 +++ b/src/windows/sac-enabler.ps1 @@ -1,82 +1,283 @@ <# .SYNOPSIS Enables Special Administration Console (SAC) and Serial Console boot settings. + .DESCRIPTION - Configures BCD to enable the boot menu, set a timeout, and turn on EMS/SAC - to allow serial console access to the VM. - Created by Tony.Mocanu@Microsoft.com + This script runs from a rescue VM to enable SAC/EMS on an attached OS disk's BCD store. + It performs the following steps: + 1. Enumerates attached partitions via Get-Disk-Partitions to locate the BCD store and OS loader. + 1a. For Gen2 disks where the EFI partition has no drive letter, uses diskpart to + temporarily assign one so the BCD store can be accessed. + 2. Identifies the default boot entry GUID from the BCD bootmgr displayorder. + 3. Logs the BCD configuration before any changes are made. + 4. Enables the boot menu with a 5-second timeout (displaybootmenu, timeout). + 5. Enables Boot EMS on the boot manager (bootems yes). + 6. Enables EMS on the default OS entry (ems ON). + 7. Configures EMS settings for serial console (EMSPORT:1, EMSBAUDRATE:115200). + 8. Logs the BCD configuration after changes for verification. + +.NOTES + Name: sac-enabler.ps1 + Author: Tony.Mocanu@Microsoft.com + + .VERSION + v1.3: [May 2026] - Updated the script again (current) + - Fixed breaking exception when the Hyper-V module is not installed on the host. + - Added explicit checking via Get-Module before executing nested VM discovery. + v1.2: [May 2026] - Updated the script + - Included advanced Gen2 unlettered EFI fallback and dynamic drive-letter assignment. + v0.1: Initial commit. This was the version 1.0 of the script. + +.SCENARIO_RECREATION + To recreate a testable scenario on a rescue VM with an attached OS disk: + 1. Create a test VM in Azure and attach its OS disk to a rescue VM. + 2. The BCD store is on the System Reserved (Gen1) or EFI (Gen2) partition, which + may not have a drive letter. Find it by scanning all volumes (run as Admin): +Get-Volume | Where-Object { $_.DriveLetter } | ForEach-Object { $d = $_.DriveLetter; @("$d`:\boot\bcd","$d`:\efi\microsoft\boot\bcd") | Where-Object { Test-Path $_ } | ForEach-Object { Write-Output "FOUND: $_" } } + If nothing is found, the partition has no drive letter. For System Reserved (Gen1): +Get-Partition | Where-Object { -not $_.DriveLetter -and $_.Size -lt 1GB } | Format-Table DiskNumber, PartitionNumber, Size, Type +Set-Partition -DiskNumber -PartitionNumber -NewDriveLetter S + For EFI partitions (Gen2), Set-Partition won't work -- use diskpart instead: + diskpart + select disk + select partition + assign letter=S + exit + Then check: Test-Path S:\boot\bcd or Test-Path S:\efi\microsoft\boot\bcd + + Example with two attached disks (from Disk Management): + Disk 2 (Gen1): System Reserved (F:) 500 MB | Windows (G:) 126 GB + -> BCD already accessible at F:\boot\bcd + Disk 3 (Gen2): 450 MB (no letter) | EFI (no letter) 99 MB | Windows (H:) 126 GB + -> EFI partitions are protected; use diskpart to assign a letter: + diskpart + select disk 3 + select partition 2 + assign letter=S + exit + -> BCD at S:\efi\microsoft\boot\bcd + + 3. Once you have the BCD path, disable SAC/EMS to simulate a broken VM: + + Gen1 example (F:\boot\bcd): +bcdedit /store F:\boot\bcd /ems "{default}" OFF +bcdedit /store F:\boot\bcd /set "{bootmgr}" bootems no +bcdedit /store F:\boot\bcd /set "{bootmgr}" displaybootmenu no + + Gen2 example (S:\efi\microsoft\boot\bcd): +bcdedit /store S:\efi\microsoft\boot\bcd /ems "{default}" OFF +bcdedit /store S:\efi\microsoft\boot\bcd /set "{bootmgr}" bootems no +bcdedit /store S:\efi\microsoft\boot\bcd /set "{bootmgr}" displaybootmenu no + + 4. Verify EMS is disabled: +bcdedit /store F:\boot\bcd /enum "{default}" +bcdedit /store F:\boot\bcd /enum "{bootmgr}" + Expected: ems = No or absent, bootems = No or absent. + 5. Run the script. It should enable ems, bootems, displaybootmenu, and emssettings. + 6. Verify all SAC settings are now enabled (see .VERIFICATION section). + +.EXAMPLE + az vm repair run -g -n --run-id win-sac-enabler --run-on-repair + +.VERIFICATION + 1. Check the log file for success: +Get-ChildItem "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\sac-enabler_*.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content + Expected: "BCD AFTER SAC ENABLE" section present and return code 0 ($STATUS_SUCCESS). + 2. Manually verify the BCD store (replace drive letters with the ones found in step 2): + + Gen1 (System Reserved on F:): +bcdedit /store F:\boot\bcd /enum "{default}" +bcdedit /store F:\boot\bcd /enum "{bootmgr}" + + Gen2 (EFI partition -- use diskpart to assign a letter if needed, e.g. P:): +bcdedit /store P:\efi\microsoft\boot\bcd /enum "{default}" +bcdedit /store P:\efi\microsoft\boot\bcd /enum "{bootmgr}" + + Expected: ems = Yes on the OS entry, bootems = Yes on bootmgr, + displaybootmenu = Yes, timeout = 5, EMSPORT = 1, EMSBAUDRATE = 115200. + + NOTE: For Gen2 disks, the script automatically assigns a temporary drive letter + to the EFI System Partition via diskpart if Get-Disk-Partitions did not assign one. + The temporary letter is removed after processing. #> -# 1. Initialize script and helper functions +# Initialization . .\src\windows\common\setup\init.ps1 -. .\src\windows\common\helpers\Get-Disk-Partitions.ps1 +. .\src\windows\common\helpers\Get-Disk-Partitions-v2.ps1 -# 2. Set Log Path to Public Desktop -$logFile = "C:\Users\Public\Desktop\sac-enabler-log.txt" +# Log Configuration +$logDir = "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension" +if (-not (Test-Path $logDir)) { $null = New-Item -ItemType Directory -Path $logDir -Force } +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$logFile = "$logDir\sac-enabler_$timestamp.log" -# 3. Execution Logic -$partitionlist = Get-Disk-Partitions -Log-Info '#03 - Enumerate partitions to enable SAC' | Tee-Object -FilePath $logFile -Append +# Status Tracking +$script_final_status = $STATUS_ERROR -foreach ( $partitionGroup in $partitionlist | group DiskNumber ) -{ - $isBcdPath = $false - $bcdPath = '' - $isOsPath = $false +try { + # Check if the Hyper-V module is available before performing nested VM checks + if (Get-Module -ListAvailable -Name Hyper-V) { + $guestHyperVVirtualMachine = Get-VM -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + if ($guestHyperVVirtualMachine) { + if ($guestHyperVVirtualMachine.State -eq 'Running') { + Log-Info "Stopping nested guest VM $($guestHyperVVirtualMachine.VMName)" | Tee-Object -FilePath $logFile -Append + try { + Stop-VM $guestHyperVVirtualMachine -ErrorAction Stop -Force + } + catch { + Log-Warning "Failed to stop nested guest VM, will continue but may have limited success" | Tee-Object -FilePath $logFile -Append + } + } + } + } else { + Log-Info "Hyper-V PowerShell module is not available on this host. Skipping nested VM validation." | Tee-Object -FilePath $logFile -Append + } + + # Step 1 - Enumerate partitions to locate the BCD store and OS loader + $partitionlist = Get-Disk-Partitions + $rescueDrive = $env:SystemDrive -replace ':', '' + Log-Info 'Enumerating partitions to enable SAC...' | Tee-Object -FilePath $logFile -Append + + foreach ( $partitionGroup in $partitionlist | group DiskNumber ) + { + $isBcdPath = $false + $bcdPath = '' + $isOsPath = $false - # Discovery Logic (Matches your BCD logic for consistency) - ForEach ($drive in $partitionGroup.Group | select -ExpandProperty DriveLetter ) - { - if ( -not $isBcdPath ) + # Scan each drive for BCD store and Windows OS loader + ForEach ($drive in $partitionGroup.Group | select -ExpandProperty DriveLetter ) { - $bcdPath = $drive + ':\boot\bcd' - $isBcdPath = Test-Path $bcdPath + # Skip the rescue VM's own OS drive + if ($drive -eq $rescueDrive) { continue } + if ( -not $isBcdPath ) { - $bcdPath = $drive + ':\efi\microsoft\boot\bcd' + $bcdPath = $drive + ':\boot\bcd' $isBcdPath = Test-Path $bcdPath - } - } - if (-not $isOsPath) + if ( -not $isBcdPath ) + { + $bcdPath = $drive + ':\efi\microsoft\boot\bcd' + $isBcdPath = Test-Path $bcdPath + } + } + if (-not $isOsPath) + { + $isOsPath = Test-Path ($drive + ':\windows\system32\winload.exe') + } + } + + # Gen2 EFI fallback: if OS found but no BCD, discover unlettered EFI partition + $tempEfiLetter = $null + $tempEfiDiskNum = $null + $tempEfiPartNum = $null + if (-not $isBcdPath -and $isOsPath) { - $isOsPath = Test-Path ($drive + ':\windows\system32\winload.exe') + $diskNum = [int]$partitionGroup.Name + $rescueDiskNum = (Get-Partition -DriveLetter $rescueDrive -ErrorAction SilentlyContinue | Select-Object -First 1).DiskNumber + if ($diskNum -ne $rescueDiskNum) + { + Log-Info "Disk ${diskNum}: OS found but no BCD - checking for unlettered EFI partition (Gen2)..." | Tee-Object -FilePath $logFile -Append + $efiGptType = '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' + $efiParts = Get-Partition -DiskNumber $diskNum -ErrorAction SilentlyContinue | Where-Object { + $_.GptType -eq $efiGptType -and (-not $_.DriveLetter -or $_.DriveLetter -eq [char]0) + } + if ($efiParts) + { + # Find an available drive letter (Z downward to avoid conflicts) + $usedLetters = @() + Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { $usedLetters += $_.DriveLetter } + $tempLetter = $null + foreach ($l in @('Z','Y','X','W','V','U','T','S','R','Q')) { + if ($l -notin $usedLetters) { $tempLetter = $l; break } + } + if ($tempLetter) + { + foreach ($ep in $efiParts) + { + $pn = $ep.PartitionNumber + Log-Info "Assigning temp letter ${tempLetter}: to Disk $diskNum Partition $pn (EFI)..." | Tee-Object -FilePath $logFile -Append + $dpLines = @("select disk $diskNum", "select partition $pn", "assign letter=$tempLetter") + $dpLines | diskpart | Out-Null + Start-Sleep -Seconds 2 + $bcdPath = "${tempLetter}:\efi\microsoft\boot\bcd" + $isBcdPath = Test-Path $bcdPath + if ($isBcdPath) + { + Log-Info "Found Gen2 BCD store at $bcdPath" | Tee-Object -FilePath $logFile -Append + $tempEfiLetter = $tempLetter + $tempEfiDiskNum = $diskNum + $tempEfiPartNum = $pn + break + } + else + { + Log-Info "No BCD at $bcdPath, removing letter..." | Tee-Object -FilePath $logFile -Append + $dpRemove = @("select disk $diskNum", "select partition $pn", "remove letter=$tempLetter") + $dpRemove | diskpart | Out-Null + } + } + } + else + { + Log-Warning "No available drive letter for EFI partition on Disk $diskNum" | Tee-Object -FilePath $logFile -Append + } + } + } } - } - # 4. Apply SAC Changes if BCD is found - if ( $isBcdPath -and $isOsPath ) - { - # Capture the target ID (usually {default}) - $bcdout = bcdedit /store $bcdPath /enum bootmgr /v - $defaultLine = $bcdout | Select-String 'displayorder' | select -First 1 - - if ($defaultLine -match '\{([^}]+)\}') { - $defaultId = $matches[0] - - Log-Output "--- BCD BEFORE SAC ENABLE ---" | Tee-Object -FilePath $logFile -Append - $beforeBcd = bcdedit /store $bcdPath /enum $defaultId - foreach ($line in $beforeBcd) { if ($line.Trim()) { Log-Output $line | Tee-Object -FilePath $logFile -Append } } - - Log-Info "Applying SAC and EMS configurations..." | Tee-Object -FilePath $logFile -Append - - # Core Logic from Original Script - bcdedit /store $bcdPath /set "{bootmgr}" displaybootmenu yes | Out-Null - bcdedit /store $bcdPath /set "{bootmgr}" timeout 5 | Out-Null - bcdedit /store $bcdPath /set "{bootmgr}" bootems yes | Out-Null - bcdedit /store $bcdPath /ems $defaultId ON | Out-Null - $res = bcdedit /store $bcdPath /emssettings EMSPORT:1 EMSBAUDRATE:115200 - - Log-Output "Result: $res" | Tee-Object -FilePath $logFile -Append - - # --- AFTER CHANGE (Line-by-Line Logging) --- - Log-Output "--- BCD AFTER SAC ENABLE ---" | Tee-Object -FilePath $logFile -Append - $afterBcd = bcdedit /store $bcdPath /enum $defaultId - foreach ($line in $afterBcd) { if ($line.Trim()) { Log-Output $line | Tee-Object -FilePath $logFile -Append } } + # Apply SAC changes if both BCD and OS loader were found + if ( $isBcdPath -and $isOsPath ) + { + # Step 2 - Identify the default boot entry GUID + $bcdout = bcdedit /store $bcdPath /enum bootmgr /v + $defaultLine = $bcdout | Select-String 'displayorder' | select -First 1 - return $STATUS_SUCCESS + if ($defaultLine -match '\{([^}]+)\}') { + $defaultId = $matches[0] + + # Step 3 - Log BCD configuration before changes + Log-Output "--- BCD BEFORE SAC ENABLE ---" | Tee-Object -FilePath $logFile -Append + $beforeBcd = bcdedit /store $bcdPath /enum $defaultId + foreach ($line in $beforeBcd) { if ($line.Trim()) { Log-Output $line | Tee-Object -FilePath $logFile -Append } } + + # Steps 4-7 - Enable boot menu, Boot EMS, EMS on OS entry, and EMS serial settings + Log-Info "Applying SAC and EMS configurations..." | Tee-Object -FilePath $logFile -Append + bcdedit /store $bcdPath /set "{bootmgr}" displaybootmenu yes | Out-Null + bcdedit /store $bcdPath /set "{bootmgr}" timeout 5 | Out-Null + bcdedit /store $bcdPath /set "{bootmgr}" bootems yes | Out-Null + bcdedit /store $bcdPath /ems $defaultId ON | Out-Null + $res = bcdedit /store $bcdPath /emssettings EMSPORT:1 EMSBAUDRATE:115200 + + Log-Output "Result: $res" | Tee-Object -FilePath $logFile -Append + + # Step 8 - Log BCD configuration after changes for verification + Log-Output "--- BCD AFTER SAC ENABLE ---" | Tee-Object -FilePath $logFile -Append + $afterBcd = bcdedit /store $bcdPath /enum $defaultId + foreach ($line in $afterBcd) { if ($line.Trim()) { Log-Output $line | Tee-Object -FilePath $logFile -Append } } + + $script_final_status = $STATUS_SUCCESS + } + } + + # Clean up temporary EFI drive letter if one was assigned + if ($tempEfiLetter) + { + Log-Info "Removing temp letter ${tempEfiLetter}: from Disk $tempEfiDiskNum Partition $tempEfiPartNum" | Tee-Object -FilePath $logFile -Append + $dpClean = @("select disk $tempEfiDiskNum", "select partition $tempEfiPartNum", "remove letter=$tempEfiLetter") + $dpClean | diskpart | Out-Null } } + + if ($script_final_status -ne $STATUS_SUCCESS) { + Log-Error "FAILED: Script could not find a valid OS disk to enable SAC." | Tee-Object -FilePath $logFile -Append + } +} +catch { + Log-Error "An error occurred: $($_.Exception.Message)" | Tee-Object -FilePath $logFile -Append + $script_final_status = $STATUS_ERROR +} +finally { + Log-Info "Script ended at $(Get-Date)" | Tee-Object -FilePath $logFile -Append } -Log-Error "FAILED: Script could not find a valid OS disk to enable SAC." | Tee-Object -FilePath $logFile -Append -return $STATUS_ERROR +return $script_final_status