Apply Windows Updates Via a Chef Recipe

Summary

Using Chef (or any other remote management tool for that matter – like Windows Remote Management or PowerShell Remoting) to apply Windows updates to a remote system is difficult because some of the Windows Update methods will not work when executed from a remote connection, even if you’re using Administrator level credentials (this is apparently a feature, not a bug). To get around this, the Chef recipe must launch the update commands via a task in Task Scheduler. This can be done by configuring the Task Scheduler task to call the Chef recipe via the local ‘chef-client’ utility.

In this example I’m creating a task to ‘run once’, but since it’s in the past, this task will never get executed on its own. Then I’m manually launching the newly created task, which just calls the Chef Client to run my InstallWindowsUpdates cookbook (recipe: default.rb).

Create/Execute Task via PowerShell Remoting Example:

Invoke-Command -ComputerName <server name> -Credential <admin credentials> -ScriptBlock { cmd /c "schtasks /Create /RU System /RL HIGHEST /F /TR ""c:\opscode\chef\bin\chef-client.bat -o InstallWindowsUpdates"" /TN ChefInstallUpdates /SC Once /ST 00:00 2>&1" }
Invoke-Command -ComputerName <server name> -Credential <admin credentials> -ScriptBlock { cmd /c "schtasks /run /tn ChefInstallUpdates 2>&1" }

Windows Update Recipe Example (default.rb):

#
# Cookbook Name:: InstallWindowsUpdates
# Recipe:: default
# Author(s):: Otto Helweg
#

# Configures Windows Update automatic updates
powershell_script "install-windows-updates" do
  guard_interpreter :powershell_script
  # Set a 2 hour timeout
  timeout 7200
  code <<-EOH
    Write-Host -ForegroundColor Green "Searching for updates (this may take up to 30 minutes or more)..."

    $updateSession = New-Object -com Microsoft.Update.Session
    $updateSearcher = $updateSession.CreateupdateSearcher()
    try
    {
      $searchResult =  $updateSearcher.Search("Type='Software' and IsHidden=0 and IsInstalled=0").Updates
    }
    catch
    {
      eventcreate /t ERROR /ID 1 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: Update attempt failed."
      $updateFailed = $true
    }

    if(!($updateFailed)) {
      foreach ($updateItem in $searchResult) {
        $UpdatesToDownload = New-Object -com Microsoft.Update.UpdateColl
        if (!($updateItem.EulaAccepted)) {
          $updateItem.AcceptEula()
        }
        $UpdatesToDownload.Add($updateItem)
        $Downloader = $UpdateSession.CreateUpdateDownloader()
        $Downloader.Updates = $UpdatesToDownload
        $Downloader.Download()
        $UpdatesToInstall = New-Object -com Microsoft.Update.UpdateColl
        $UpdatesToInstall.Add($updateItem)
        $Title = $updateItem.Title
        Write-host -ForegroundColor Green "  Installing Update: $Title"
        $Installer = $UpdateSession.CreateUpdateInstaller()
        $Installer.Updates = $UpdatesToInstall
        $InstallationResult = $Installer.Install()
        eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: Installed update $Title."
      }

      if (!($searchResult.Count)) {
        eventcreate /t INFORMATION /ID 999 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: No updates available."
      }
      eventcreate /t INFORMATION /ID 1 /L APPLICATION /SO "Chef-Cookbook" /D "InstallWindowsUpdates: Done Installing Updates."
    }
  EOH
  action :run
end

Enjoy!

Trigger a PowerShell Script from a Windows Event

Note: Portions of this blog are taken from an old blog post titled “Reference the Event That Triggered Your Task”

This example will demonstrate both how to trigger ﴾launch﴿ a PowerShell script from a specific Windows Event, AND pass parameters to the PowerShell script from the Windows Event that triggered the script. For the purpose of this example, a test event will be generated using the built‐in EventCreate command‐line utility.

Background: The scenario behind this example was a need to clean up a file‐share after a specific Windows Event occurred. A specific Windows Event was logged upon the success of a file watermarking process. The event used in this example loosely follows the original event format.

The following steps will be demonstrated:

  1. Manually create the trigger event.
  2. Use Event Viewer to create an event triggered task from the above event.
  3. Modify the task to expose event details to the downstream script.
  4. Implement the PowerShell script to be triggered.
  5. Verify the setup.

Step 1: Create the trigger event using EventCreate ﴾it’s easier to go this route to generate a Scheduled Task for modification rather than trying to create one from scratch﴿. From the command prompt run:

eventcreate /T INFORMATION /SO SomeApplication /ID 1000 /L APPLICATION /D "<Params><Timestamp>2011-08-29T21:24:03Z</Timestamp><InputFile>C:\temp\Some Test File.txt</InputFile><Result>Success</Result></Params>"

Step 2: Use the Event Viewer “Attach Task to This Event…” feature to create the task.

Launch “Event Viewer” and find the event you created in Step 1. It should be located toward the top of the “Windows Logs\Application” Log. Once found, right‐click on the event and select “Attach Task to This Event…” then use the defaults for the first couple screens of the wizard.

PS-Trigger-1

Create a task to “Start a Program” with the following parameters:

  • Program/script: PowerShell.exe
  • Add arguments: .\TriggerScript.ps1 -eventRecordID $(eventRecordID) -eventChannel $(eventChannel)
  • Start in ﴾you might need to create this directory or alter the steps to use a directory of your choice﴿: c:\temp

PS-Trigger-2

Step 3: Modify the task to expose details about the trigger event and pass them to the PowerShell script

From within Task Scheduler, export the newly created task ﴾as an XML file﴿. Right‐click on the task “Application_SomeApplication_1000” in the “Event Viewer Tasks” folder, and select “Export…”.

PS-Trigger-3

Use Notepad ﴾or your text editor of choice ‐keep in mind the text editor must honor unicode which notepad does﴿ to add the Event parameters you which to pass along to your task. The event parameters below are the most useful for event identification. Notice the entire node

<ValueQueries>
     <Value name="eventChannel">Event/System/Channel</Value>
     <Value name="eventRecordID">Event/System/EventRecordID</Value>
     <Value name="eventSeverity">Event/System/Level</Value>
</ValueQueries>

See below:

PS-Trigger-5

From an Elevated Command Prompt, execute the following commands to delete the Trigger Task and recreate it with the newly modified exported Trigger Task ﴾I don’t believe there’s a way to modify an existing task using an updated XML file﴿. From a command prompt, run:

schtasks /delete /TN "Event Viewer Tasks\Application_SomeApplication_1000"
schtasks /create /TN "Event Viewer Tasks\Application_SomeApplication_1000" /XML Application_SomeApplication_1000.xml

Step 4: Implement the PowerShell script to be triggered by creating a script called “TriggerScript.ps1” below

Note: The script below is passed basic information about the event that triggered it. The script then queries the Windows Event Log to get more details about the event (the event payload). For this example, XML is used in the payload to separate the parameters, but any text can be passed as long as the script knows how to parse it. In addition, the “eventRecordID” that’s passed to the script should not be confused with the eventID of the event. The eventRecordID is a sequential number assigned to all events as they are logged to a specific channel. In addition, eventRecordIDs are only unique for a specific Channel (Log).

# Script Name: TriggerScript.ps1
# Usage Example (use a valid ID found via Event Viewer XML view of an event): powershell .\TriggerScript.ps1 eventRecordID 1 eventChannel Application
#
# Create a fake event for testing with the following command (from an elevated command prompt):
# eventcreate /T INFORMATION /SO SomeApplication /ID 1000 /L APPLICATION /D "<Params><Timestamp>20110829T21:24:03Z</Timestamp><InputFile>C:\temp\Some Test File.txt</InputFile><Result>Success</Result></Params>"
# Collects all named paramters (all others end up in $Args)
param($eventRecordID,$eventChannel)
$event = get-winevent -LogName $eventChannel -FilterXPath "<QueryList><Query Id='0' Path='$eventChannel'><Select Path='$eventChannel'>*[System [(EventRecordID=$eventRecordID)]]</Select></Query></QueryList>"
[xml]$eventParams = $event.Message
if ($eventParams.Params.TimeStamp) {
  [datetime]$eventTimestamp = $eventParams.Params.TimeStamp
  $eventFile = $eventParams.Params.InputFile

  $popupObject = new-object -comobject wscript.shell
  $popupObject.popup("RecordID: " + $eventRecordID + ", Channel: " + $eventChannel + ", Event Timestamp: " + $eventTimestamp + ", File: " + $eventFile)
}

Note: Besides executing a script, a task can display a popup directly or send an email. An email can be useful for catching infrequent events on your system or in your environment. And a task can be deployed via Group Policy Preferences.

Step 5: Verify the setup by generating another trigger event as in Step 1

eventcreate /T INFORMATION /SO SomeApplication /ID 1000 /L APPLICATION /D "<Params><Timestamp>2011-08-29T21:24:03Z</Timestamp><InputFile>C:\temp\Some Test File.txt</InputFile><Result>Success</Result></Params>"

You should see the following Popup window appear ﴾it might be hidden behind other Windows﴿:

PS-Trigger-4

Enjoy!