src/windows/win-get-patches.ps1 (111 lines of code) (raw):

###################################################################################################### # # .SYNOPSIS # Get installed Windows patches for the nested Hyper-V server on a Rescue VM. # # .DESCRIPTION # Get installed Windows patches for the nested Hyper-V server on a Rescue VM using DISM. This will be helpful if the attached OS disk is from a VM that is in a nonboot state due to corrupted/failing updates. # Collects patches on the attached OS disk using DISM to verify which ones have successfully installed and which ones are failing to install. Prints them to the console and saves them to the rescue VM as a text file. # Public doc: https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/dism-operating-system-package-servicing-command-line-options?view=windows-11#get-packages # .RESOLVES # Collecting patches that fail to install is difficult when the OS refuses to boot. This will be helpful to run on the repair VM if the attached OS disk is from a VM that is in a nonboot state due to corrupted/failing updates as the # failing package can then be manually passed to win-remove-patch. # # .EXAMPLE # <# Get installed patches #> # az vm repair run -g 'sourceRG' -n 'sourceVM' --run-id 'win-get-patches' --verbose --run-on-repair # # .NOTES # Author: Ryan McCallum # # .VERSION # v0.1: Initial commit # ####################################################################################################### # Initialize script . .\src\windows\common\setup\init.ps1 . .\src\windows\common\helpers\Get-Disk-Partitions.ps1 # Declare variables $scriptStartTime = get-date -f yyyyMMddHHmmss $scriptPath = split-path -path $MyInvocation.MyCommand.Path -parent $scriptName = (split-path -path $MyInvocation.MyCommand.Path -leaf).Split('.')[0] $logFile = "$env:PUBLIC\Desktop\$($scriptName).log" $scriptStartTime | Tee-Object -FilePath $logFile -Append Log-Output "START: Running script win-get-patches" | Tee-Object -FilePath $logFile -Append try { # Make sure guest VM is shut down if it exists $features = get-windowsfeature -ErrorAction Stop $hyperv = $features | where Name -eq 'Hyper-V' $hypervTools = $features | where Name -eq 'Hyper-V-Tools' $hypervPowerShell = $features | where Name -eq 'Hyper-V-Powershell' $dhcp = $features | where Name -eq 'DHCP' $rsatDhcp = $features | where Name -eq 'RSAT-DHCP' if ($hyperv.Installed -and $hypervTools.Installed -and $hypervPowerShell.Installed) { $guestHyperVVirtualMachine = Get-VM $guestHyperVVirtualMachineName = $guestHyperVVirtualMachine.VMName if ($guestHyperVVirtualMachine) { if ($guestHyperVVirtualMachine.State -eq 'Running') { Log-Output "#01 - Stopping nested guest VM $guestHyperVVirtualMachineName" | Tee-Object -FilePath $logFile -Append Stop-VM $guestHyperVVirtualMachine -ErrorAction Stop -Force } } else { Log-Output "#01 - No running nested guest VM, continuing script" | Tee-Object -FilePath $logFile -Append } } # Make sure the disk is online Log-Output "#02 - Bringing disk online" | Tee-Object -FilePath $logFile -Append $disk = get-disk -ErrorAction Stop | Where-Object { $_.FriendlyName -eq 'Msft Virtual Disk' } $disk | set-disk -IsOffline $false -ErrorAction Stop # Handle disk partitions $partitionlist = Get-Disk-Partitions $partitionGroup = $partitionlist | Group-Object DiskNumber Log-Output '#03 - enumerate partitions for boot config' | Tee-Object -FilePath $logFile -Append forEach ( $partitionGroup in $partitionlist | Group-Object DiskNumber ) { # Reset paths for each part group (disk) $isBcdPath = $false $bcdPath = '' $isOsPath = $false $osPath = '' # Scan all partitions of a disk for bcd store and os file location ForEach ($drive in $partitionGroup.Group | Select-Object -ExpandProperty DriveLetter ) { # Check if no bcd store was found on the previous partition already if ( -not $isBcdPath ) { $bcdPath = $drive + ':\boot\bcd' $isBcdPath = Test-Path $bcdPath # If no bcd was found yet at the default location look for the uefi location too if ( -not $isBcdPath ) { $bcdPath = $drive + ':\efi\microsoft\boot\bcd' $isBcdPath = Test-Path $bcdPath } } # Check if os loader was found on the previous partition already if (-not $isOsPath) { $osPath = $drive + ':\windows\system32\winload.exe' $isOsPath = Test-Path $osPath } } # If on the OS directory, continue script if ( $isOsPath ) { Log-Output "#04 - Found OS directory at $($drive), getting patches..." | Tee-Object -FilePath $logFile -Append cmd /c "dism /image:$($drive):\ /get-packages /format:list" | Out-File -FilePath $logFile -Append # cmd /c "dism /image:$($drive):\ /get-packages /format:table" | Tee-Object -FilePath $logFile -Append $packages = (Get-WindowsPackage -path "$($drive):").packagename | ft -AutoSize Write-Output "`n" Write-Output $packages Write-Output "`n" Log-Output "Only displaying package names for brevity" Log-Output "Full DISM output is on the Rescue VM in $($logFile)" return $STATUS_SUCCESS } } } catch { Log-Error "END: Script failed with error: $_" | Tee-Object -FilePath $logFile -Append throw $_ return $STATUS_ERROR } finally { $scriptEndTime = get-date -f yyyyMMddHHmmss $scriptEndTime | Tee-Object -FilePath $logFile -Append }