Pages

Sunday, January 31, 2016

Auto Updating a New Windows 7 Installation

There are many reasons you may create a clean Windows installation:

  • Gold image for deployment
  • Lab installation for testing
  • Reinstall/recovery

Installing all the Windows updates is tedious, especially with reboots. I don’t know why Microsoft doesn’t include a big “Bring up to date” button that would let you configure for reboots. I figured someone had this sorted, and started searching.

I can’t take credit for the PowerShell script, but am glad to have found it. It does have one oddity: it attempts to disable OpenSSHd. I don’t know if that’s a weird security leftover, or a precaution by the author. The script still runs even if the OpenSSHd service isn’t found, which it shouldn’t be. It’s not part of Windows.

I made these changes:

  1. Commented out OpenSSH commands
  2. After searching for updates, I selectively hide updates such as KB3035583, which is the Windows 10 advertisement (not needed in my case).

Here’s the link to the original script by Joe Fitzgerald:

https://gist.github.com/joefitzgerald/8203265

And the script itself, with my changes. To execute the script, right-click and choose Run with PowerShell. Or

  1. open admin command prompt
  2. cd to location of script file
  3. > powershell.exe -ExecutionPolicy ByPass -File win-update.ps1

 

param($global:RestartRequired=0,
        $global:MoreUpdates=0,
        $global:MaxCycles=10)

function Check-ContinueRestartOrEnd() {
    $RegistryKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
    $RegistryEntry = "InstallWindowsUpdates"
    switch ($global:RestartRequired) {
        0 {			
            $prop = (Get-ItemProperty $RegistryKey).$RegistryEntry
            if ($prop) {
                Write-Host "Restart Registry Entry Exists - Removing It"
                Remove-ItemProperty -Path $RegistryKey -Name $RegistryEntry -ErrorAction SilentlyContinue
            }
            
            Write-Host "No Restart Required"
            Check-WindowsUpdates
            
            if (($global:MoreUpdates -eq 1) -and ($script:Cycles -le $global:MaxCycles)) {
                Stop-Service $script:ServiceName -Force
                Set-Service -Name $script:ServiceName -StartupType Disabled -Status Stopped 
                Install-WindowsUpdates
            } elseif ($script:Cycles -gt $global:MaxCycles) {
                Write-Host "Exceeded Cycle Count - Stopping"
			} else {
                Write-Host "Done Installing Windows Updates"
                Set-Service -Name $script:ServiceName -StartupType Automatic -Status Running         
            }
        }
        1 {
            $prop = (Get-ItemProperty $RegistryKey).$RegistryEntry
            if (-not $prop) {
                Write-Host "Restart Registry Entry Does Not Exist - Creating It"
                Set-ItemProperty -Path $RegistryKey -Name $RegistryEntry -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -File $($script:ScriptPath)"
            } else {
                Write-Host "Restart Registry Entry Exists Already"
            }
            
            Write-Host "Restart Required - Restarting..."
            Restart-Computer
        }
        default { 
            Write-Host "Unsure If A Restart Is Required" 
            break
        }
    }
}

function Install-WindowsUpdates() {
    $script:Cycles++
    Write-Host 'Evaluating Available Updates:'
    $UpdatesToDownload = New-Object -ComObject 'Microsoft.Update.UpdateColl'
    foreach ($Update in $SearchResult.Updates) {
        if (($Update -ne $null) -and (!$Update.IsDownloaded)) {
            [bool]$addThisUpdate = $false
            if ($Update.InstallationBehavior.CanRequestUserInput) {
                Write-Host "SKIPPING: $($Update.Title) because it requires user input"
            } else {
                if (!($Update.EulaAccepted)) {
                    Write-Host "> Note: $($Update.Title) has a license agreement that must be accepted. Accepting the license."
                    $Update.AcceptEula()
                    [bool]$addThisUpdate = $true
                } else {
                    [bool]$addThisUpdate = $true
                }
            }
        
            if ([bool]$addThisUpdate) {
                Write-Host "Adding: $($Update.Title)"
                $UpdatesToDownload.Add($Update) |Out-Null
            }
		}
    }
    
    if ($UpdatesToDownload.Count -eq 0) {
        Write-Host "No Updates To Download..."
    } else {
        Write-Host 'Downloading Updates...'
        $Downloader = $UpdateSession.CreateUpdateDownloader()
        $Downloader.Updates = $UpdatesToDownload
        $Downloader.Download()
    }
	
    $UpdatesToInstall = New-Object -ComObject 'Microsoft.Update.UpdateColl'
    [bool]$rebootMayBeRequired = $false
    Write-Host 'The following updates are downloaded and ready to be installed:'
    foreach ($Update in $SearchResult.Updates) {
        if ($Update.IsDownloaded) {
            Write-Host "> $($Update.Title)"
            $UpdatesToInstall.Add($Update) |Out-Null
              
            if ($Update.InstallationBehavior.RebootBehavior -gt 0){
                [bool]$rebootMayBeRequired = $true
            }
        }
    }
    
    if ($UpdatesToInstall.Count -eq 0) {
        Write-Host 'No updates available to install. Press any key.'
        $global:MoreUpdates=0
        $global:RestartRequired=0
        #wait for user input, so message is seen
        $wait = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
        break
    }

    if ($rebootMayBeRequired) {
        Write-Host 'These updates may require a reboot'
        $global:RestartRequired=1
    }
	
    Write-Host 'Installing updates...'
  
    $Installer = $script:UpdateSession.CreateUpdateInstaller()
    $Installer.Updates = $UpdatesToInstall
    $InstallationResult = $Installer.Install()
  
    Write-Host "Installation Result: $($InstallationResult.ResultCode)"
    Write-Host "Reboot Required: $($InstallationResult.RebootRequired)"
    Write-Host 'Listing of updates installed and individual installation results:'
    if ($InstallationResult.RebootRequired) {
        $global:RestartRequired=1
    } else {
        $global:RestartRequired=0
    }
    
    for($i=0; $i -lt $UpdatesToInstall.Count; $i++) {
        New-Object -TypeName PSObject -Property @{
            Title = $UpdatesToInstall.Item($i).Title
            Result = $InstallationResult.GetUpdateResult($i).ResultCode
        }
    }
	
    Check-ContinueRestartOrEnd
}

function Check-WindowsUpdates() {
    Write-Host "Checking For Windows Updates"
    $Username = $env:USERDOMAIN + "\" + $env:USERNAME
 
    New-EventLog -Source $ScriptName -LogName 'Windows Powershell' -ErrorAction SilentlyContinue
 
    $Message = "Script: " + $ScriptPath + "`nScript User: " + $Username + "`nStarted: " + (Get-Date).toString()

    Write-EventLog -LogName 'Windows Powershell' -Source $ScriptName -EventID "104" -EntryType "Information" -Message $Message
    Write-Host $Message

    $script:UpdateSearcher = $script:UpdateSession.CreateUpdateSearcher()
    $script:SearchResult = $script:UpdateSearcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0")      
    if ($SearchResult.Updates.Count -ne 0) {
        $script:SearchResult.Updates |Select-Object -Property Title, Description, SupportUrl, UninstallationNotes, RebootRequired, EulaAccepted |Format-List
        $global:MoreUpdates=1
    } else {
        Write-Host 'There are no applicable updates'
        $global:RestartRequired=0
        $global:MoreUpdates=0
    }
    
    #clf hide undesired updates
    foreach ($Update in $SearchResult.Updates) {
        if ($Update.Title.Contains("KB3035583")) {
            Write-Host "Hiding $($Update.Title) because it's the Windows 10 Notification."
            $Update.IsHidden = $TRUE
        }
    }
}

$script:ScriptName = $MyInvocation.MyCommand.ToString()
$script:ScriptPath = $MyInvocation.MyCommand.Path
$script:UpdateSession = New-Object -ComObject 'Microsoft.Update.Session'
$script:UpdateSession.ClientApplicationID = 'Packer Windows Update Installer'
$script:UpdateSearcher = $script:UpdateSession.CreateUpdateSearcher()
$script:SearchResult = New-Object -ComObject 'Microsoft.Update.UpdateColl'
$script:Cycles = 0

# $script:ServiceName = "OpenSSHd"

# Stop-Service $script:ServiceName -Force
# Set-Service -Name $script:ServiceName -StartupType Disabled -Status Stopped 

Check-WindowsUpdates
if ($global:MoreUpdates -eq 1) {
    Install-WindowsUpdates
} else {
    Check-ContinueRestartOrEnd
}

 

References

An alternative method, clever but not as flexible.

http://serverfault.com/questions/320750/best-way-to-fully-update-a-new-installed-windows

WSUS Update class

https://msdn.microsoft.com/en-us/library/windows/desktop/aa386099(v=vs.85).aspx

Powershell ReadKey doesn’t work when using editor.

https://social.technet.microsoft.com/Forums/windowsserver/en-US/0787bcba-f977-400d-8b62-57ec64a71a8a/why-do-i-get-an-exception-calling-readkey?forum=winserverpowershell