Skip to content

Instantly share code, notes, and snippets.

@Chirishman
Last active March 12, 2019 16:51
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Chirishman/7c3dfa37757f9e4e60fffa0b8f2a55e0 to your computer and use it in GitHub Desktop.
Save Chirishman/7c3dfa37757f9e4e60fffa0b8f2a55e0 to your computer and use it in GitHub Desktop.
Class Based DSC resource for patching and rebooting during specified maintenance windows
class MaintenanceWindow {
[Bool]$Enabled = $true
[DateTime]$StartTime
[DateTime]$EndTime
[System.DayOfWeek[]]$DaysOfWeek = @([System.DayOfWeek]::Saturday, [System.DayOfWeek]::Sunday)
[ValidateSet('Weekly', 'Daily')][string]$Frequency
MaintenanceWindow ([DateTime]$StartTime, [DateTime]$EndTime, [String]$Frequency) {
$this.StartTime = $StartTime
$this.EndTime = $EndTime
$this.Frequency = $Frequency
if ( $Frequency -eq 'Daily' ) {
$this.DaysOfWeek = ([system.dayofweek].DeclaredMembers.Name | Where-Object {$_ -notmatch '_'})
}
}
MaintenanceWindow ([DateTime]$StartTime, [DateTime]$EndTime, [String]$Frequency, [System.DayOfWeek[]]$DaysOfWeek) {
$this.StartTime = $StartTime
$this.EndTime = $EndTime
$this.DaysOfWeek = $DaysOfWeek
$this.Frequency = $Frequency
}
MaintenanceWindow ([MaintenanceWindow]$MaintenanceWindow) {
$this.Enabled = $MaintenanceWindow.Enabled
$this.StartTime = $MaintenanceWindow.StartTime
$this.EndTime = $MaintenanceWindow.EndTime
$this.DaysOfWeek = $MaintenanceWindow.DaysOfWeek
$this.Frequency = $MaintenanceWindow.Frequency
}
MaintenanceWindow ([String]$JSON) {
$ParsedJSON = $JSON | ConvertFrom-Json
$this.Enabled = $ParsedJSON.Enabled
$this.StartTime = $this.ConvertUTCtoLocal($ParsedJSON.StartTime)
$this.EndTime = $this.ConvertUTCtoLocal($ParsedJSON.EndTime)
$this.DaysOfWeek = $ParsedJSON.DaysOfWeek
$this.Frequency = $ParsedJSON.Frequency
}
[datetime] ConvertUTCtoLocal($UTCTime) {
$strCurrentTimeZone = (Get-WmiObject win32_timezone).StandardName
$TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone)
return ([System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ))
}
[string] ToString() {
return ($this | ConvertTo-Json -Compress)
}
[bool] CheckWindow() {
$Now = [datetime]::Now
Write-Verbose "Current Timestamp: $now"
Write-Verbose "Days of Week: $($this.DaysOfWeek)"
Write-Verbose "StartTime: $($this.StartTime.TimeOfDay)"
Write-Verbose "EndTime: $($this.EndTime.TimeOfDay)"
$start = $this.StartTime.ToString('tt')
$end = $this.EndTime.ToString('tt')
$NowWindow = $Now.ToString('tt')
$Night = 'PM'
[bool]$InWindow = $false
$this.DaysOfWeek | ForEach-Object {
$thisDayOfWeek = $_
[bool]$EndCheck = ($End -ne $Night)
if (
$Now.DayOfWeek -in @($thisDayOfWeek,(($thisDayOfWeek + (1 * $EndCheck)) % 7)) -and
$(
if (($Start -eq $Night -or $EndCheck) -and $NowWindow -eq $Night) {
$Now.TimeOfDay -gt $this.StartTime.TimeOfDay
} else {
$true
}
) -and
$(
if (($End -eq $Night -and $NowWindow -eq $Night) -or ($Start -ne $Night)) {
$Now.TimeOfDay -lt $this.EndTime.TimeOfDay
} else {
$true
}
)
) {
Set-Variable -Name InWindow -Value $true -WhatIf:$false
}
}
if ( $InWindow ) {
Write-Verbose 'Within Maintenance Window - Initiating Pending Action'
return $false
} else {
Write-Verbose 'Not Within Maintenance Window - Skipping Pending Action'
return $true
}
}
}
[DscResource()]
class MaintenanceReboot {
[DscProperty(Key)]
[string]$Name
[DscProperty()]
[String]$MaintenanceWindow
[DscProperty()]
[boolean]$SkipComponentBasedServicing = $false
[DscProperty(NotConfigurable)]
[boolean]$ComponentBasedServicing
[DscProperty()]
[boolean]$SkipWindowsUpdate = $false
[DscProperty(NotConfigurable)]
[boolean]$WindowsUpdate
[DscProperty()]
[boolean]$SkipPendingFileRename = $false
[DscProperty(NotConfigurable)]
[boolean]$PendingFileRename
[DscProperty()]
[boolean]$SkipPendingComputerRename = $false
[DscProperty(NotConfigurable)]
[boolean]$PendingComputerRename
[DscProperty()]
[boolean]$SkipCcmClientSDK = $false
[DscProperty(NotConfigurable)]
[boolean]$CcmClientSDK
[MaintenanceReboot] Get() {
$ComponentBasedServicingKeys = (Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\').Name
$this.ComponentBasedServicing = $ComponentBasedServicingKeys -Split "\\" -contains "RebootPending"
$WindowsUpdateKeys = (Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\').Name
$this.WindowsUpdate = $WindowsUpdateKeys -Split "\\" -contains "RebootRequired"
$this.PendingFileRename = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\').PendingFileRenameOperations.Length -gt 0
$ActiveComputerName = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName').ComputerName
$PendingComputerName = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName').ComputerName
$this.PendingComputerRename = $ActiveComputerName -ne $PendingComputerName
if (-not $this.SkipCcmClientSDK) {
$CCMSplat = @{
NameSpace = 'ROOT\ccm\ClientSDK'
Class = 'CCM_ClientUtilities'
Name = 'DetermineIfRebootPending'
ErrorAction = 'Stop'
}
$this.CcmClientSDK = $(
Try {
Invoke-WmiMethod @CCMSplat
}
Catch {
Write-Warning "Unable to query CCM_ClientUtilities: $_"
}
) | ForEach-Object {(($_.ReturnValue -eq 0) -and ($_.IsHardRebootPending -or $_.RebootPending))}
} #CCM_ClientUtilities querey
return $this
}
Set() {
Set-Variable -Name DSCMachineStatus -Scope Global -Value 1
}
[bool] Test() {
$status = $this.Get()
$RebootsFound = $false
@(
@('ComponentBasedServicing', 'Pending component based servicing reboot found.'),
@('WindowsUpdate', 'Pending Windows Update reboot found.'),
@('PendingFileRename', 'Pending file rename found.'),
@('PendingComputerRename', 'Pending computer rename found.')
) | ForEach-Object {
if (-not ($Status[( -join ('Skip', $_[0]))]) -and $Status."$($_[0])") {
Write-Verbose $_[1]
Set-Variable -Name RebootsFound -Value $true -WhatIf:$false
}
}
if (-not $RebootsFound) {
Write-Verbose 'No pending reboots found.'
return $true
}
else {
Write-Verbose 'Checking Window $($status.MaintenanceWindow)'
$window = ([MaintenanceWindow]$status.MaintenanceWindow).CheckWindow()
Write-Verbose "Window: $window"
return $window
}
}
}
[DscResource()]
class MaintenancePatching {
[DscProperty(Key)]
[string]$Name
[DscProperty()]
[string]$MaintenanceWindow
[DscProperty()]
[boolean]$SingleInstance
[DscProperty()]
[ValidateSet("Security","Important","Optional")]
[String[]]$Category
[DscProperty()]
[ValidateSet("Disabled","ScheduledInstallation")]
[String]$NotificationSettings
[DscProperty(NotConfigurable)]
[string]$AutomaticUpdatesNotificationSetting
[DscProperty(NotConfigurable)]
[string]$CurrentUpdatesNotificationSetting
[DscProperty()]
[ValidateSet("WindowsUpdate","MicrosoftUpdate","WSUS")]
[String]$UpdateSource
[DscProperty(NotConfigurable)]
[String]$Source
[DscProperty()]
[boolean]$SkipCcmClientSDK
[DscProperty(NotConfigurable)]
[boolean]$CcmClientSDK
[DscProperty(NotConfigurable)]
[int]$PendingUpdates = 0
[DscProperty(NotConfigurable)]
[string]$SearchString
#TODO figure out why the hell this breaks
<# Objects of either of these types run fine when the definition is run interactively but both fail when importing/using in a configuration. A mystery for some time that isn't 10:30pm on a friday before a long weekend.
[DscProperty(NotConfigurable)]
[System.__ComObject]$Settings
[DscProperty(NotConfigurable)]
[System.__ComObject]$Session
[DscProperty(NotConfigurable)]
[System.__ComObject]$Searcher
[DscProperty(NotConfigurable)]
[System.__ComObject]$Search
[DscProperty(NotConfigurable)]
[System.__ComObject]$ServiceManager
[DscProperty(NotConfigurable)]
[System.__ComObject]$Services
[DscProperty(NotConfigurable)]
[Object]$DefaultService
[DscProperty(NotConfigurable)]
[Object]$DownloadResult
[DscProperty(NotConfigurable)]
[Object]$InstallResult
#>
[DscProperty(NotConfigurable)]
[hashtable]$ObjectHandler
[DscProperty(NotConfigurable)]
[boolean]$UpdatesFound
[DscProperty(NotConfigurable)]
[boolean]$notificationCompliant
[DscProperty(NotConfigurable)]
[boolean]$SourceCompliant
hidden [object] GetWuaWrapper([ScriptBlock] $tryBlock) {
$ExceptionReturnValue = $null
return $(
try {
$Return = Invoke-Command -ScriptBlock $tryBlock -NoNewScope
if ($Return) {
$Return
} else {
$ExceptionReturnValue
}
}
catch [System.Runtime.InteropServices.COMException]
{
switch($_.Exception.HResult)
{
# 0x8024001e -2145124322 WU_E_SERVICE_STOP Operation did not complete because the service or system was being shut down. wuerror.h
-2145124322 {
Write-Warning 'Got an error that WU service is stopping. Handling the error.'
$ExceptionReturnValue
}
# 0x8024402c -2145107924 WU_E_PT_WINHTTP_NAME_NOT_RESOLVED Same as ERROR_WINHTTP_NAME_NOT_RESOLVED - the proxy server or target server name cannot be resolved. wuerror.h
-2145107924 {
# TODO: add retry for this error
Write-Warning 'Got an error that WU could not resolve the name of the update service. Handling the error.'
$ExceptionReturnValue
}
# 0x8024401c -2145107940 WU_E_PT_HTTP_STATUS_REQUEST_TIMEOUT Same as HTTP status 408 - the server timed out waiting for the request. wuerror.h
-2145107940 {
# TODO: add retry for this error
Write-Warning 'Got an error a request timed out (http status 408 or equivalent) when WU was communicating with the update service. Handling the error.'
$ExceptionReturnValue
}
# 0x8024402f -2145107921 WU_E_PT_ECP_SUCCEEDED_WITH_ERRORS External cab file processing completed with some errors. wuerror.h
-2145107921 {
# No retry needed
Write-Warning 'Got an error that CAB processing completed with some errors.'
$ExceptionReturnValue
}
default {
throw
}
}
}
)
}
hidden [void] InitializeObjectHandler(){
$this.ObjectHandler = @{
Settings = $null
Session = $null
Searcher = $null
Search = $null
ServiceManager = $null
Services = $null
DefaultService = $null
DownloadResult = $null
InstallResult = $null
}
}
hidden [Object] GetWuaAu() {
return (New-Object -ComObject 'Microsoft.Update.AutoUpdate')
}
hidden [void] GetWuaSession() {
$this.ObjectHandler.Session = (New-Object -ComObject 'Microsoft.Update.Session')
}
hidden [void] GetWuaServiceManager() {
$this.ObjectHandler.ServiceManager = (New-Object -ComObject Microsoft.Update.ServiceManager)
$this.ObjectHandler.Services = $this.ObjectHandler.ServiceManager.Services
$this.ObjectHandler.DefaultService = @($this.ObjectHandler.Services).where{$_.IsDefaultAuService}
Write-Verbose -Message "Get default search service: $($this.ObjectHandler.DefaultService.ServiceId)"
if($this.ObjectHandler.DefaultService.ServiceId -eq '7971f918-a847-4430-9279-4a52d1efe18d')
{
$this.Source = 'MicrosoftUpdate'
}
elseif ($this.ObjectHandler.DefaultService.IsManaged) {
$this.Source = 'WSUS'
} else {
$this.Source = 'WindowsUpdate'
}
}
hidden [void] GetWuaAuSettings() {
$this.ObjectHandler.settings = ($this.GetWuaAu()).Settings
}
hidden [void] GetWuaSearchString ([bool]$security,[bool]$important,[bool]$optional) {
$securityCategoryId = "'0FA1201D-4330-4FA8-8AE9-B877473B6441'"
# security and optional and important
# not security and optional and important
$this.SearchString = $(
if($optional -and $important)
{
# Installing everything not hidden and not already installed
'IsHidden=0 and IsInstalled=0'
}
# security and optional and not important
elseif ($security -and $optional) {
# or can only be used at the top most boolean expression
"(IsAssigned=0 and IsHidden=0 and IsInstalled=0) or (CategoryIds contains $securityCategoryId and IsHidden=0 and IsInstalled=0)"
}
# security and not optional and important
elseif($security -and $important ){
# Installing everything not hidden,
# not optional (optional are not assigned) and not already installed
'IsAssigned=1 and IsHidden=0 and IsInstalled=0'
}
elseif ($optional -and $important) {
# Installing everything not hidden,
# not optional (optional are not assigned) and not already installed
'IsHidden=0 and IsInstalled=0'
}
# security and not optional and not important
elseif ($security) {
# Installing everything that is security and not hidden,
# and not already installed
"CategoryIds contains $securityCategoryId and IsHidden=0 and IsInstalled=0"
}
# not security and not optional and important
elseif ($important) {
# Installing everything that is not hidden,
# is assigned (not optional) and not already installed
# not valid cannot do not contains or a boolean not
# Note important updates will include security updates
"IsAssigned=1 and IsHidden=0 and IsInstalled=0"
}
# not security and optional and not important
elseif ($optional) {
# Installing everything that is not hidden,
# is not assigned (is optional) and not already installed
# not valid cannot do not contains or a boolean not
# Note optional updates may include security updates
"IsAssigned=0 and IsHidden=0 and IsInstalled=0"
} else {
"CategoryIds contains $securityCategoryId and IsHidden=0 and IsInstalled=0"
}
)
}
hidden [void] GetWuaSearcher() {
$this.GetWuaSearchString(($this.Category -contains 'Security'),($this.Category -contains 'Important'),($this.Category -contains 'Optional'))
$this.GetWuaWrapper(
{
$this.ObjectHandler.Searcher = $this.ObjectHandler.Session.CreateUpdateSearcher()
Write-Verbose -Message "Searching for updating using: $($this.SearchString)" -Verbose
$this.ObjectHandler.Search = $this.ObjectHandler.Searcher.Search($this.SearchString)
}
)
}
hidden [bool] TestSearchResult() {
if(!(@($this.ObjectHandler.Search | get-member |Select-Object -ExpandProperty Name) -contains 'Updates'))
{
Write-Verbose 'Did not find updates on SearchResult'
return $false
}
if(!(@(Get-Member -InputObject $this.ObjectHandler.Search.Updates |Select-Object -ExpandProperty Name) -contains 'Count'))
{
Write-Verbose 'Did not find count on updates on SearchResult'
return $false
}
return $true
}
hidden [void] GetWuaAuNotificationLevel() {
$this.CurrentUpdatesNotificationSetting = @{
0 = 'Not Configured'
1 = 'Disabled'
2 = 'Notify before download'
3 = 'Notify before installation'
4 = 'Scheduled installation'
[string]::Empty = 'Reserved'
}[$this.ObjectHandler.settings.NotificationLevel]
}
hidden [void] InvokeWuaDownloadUpdates() {
$downloader = $this.ObjectHandler.Session.CreateUpdateDownloader()
$downloader.Updates = $this.ObjectHandler.Search.Updates
Write-Verbose -Message 'Downloading updates...' -Verbose
$This.ObjectHandler.DownloadResult = $downloader.Download()
}
hidden [void] InvokeWuaInstallUpdates() {
$installer = $this.ObjectHandler.Session.CreateUpdateInstaller()
$installer.Updates = $this.ObjectHandler.Search.Updates
Write-Verbose -Message 'Installing updates...' -Verbose
$this.ObjectHandler.InstallResult = $installer.Install()
}
hidden [int] GetWuaAuNotificationLevelInt() {
$intNotificationLevel =0
switch -Regex ($this.AutomaticUpdatesNotificationSetting) {
'^Not\s*Configured$' { $intNotificationLevel = 0 }
'^Disabled$' { $intNotificationLevel = 1 }
'^Notify\s*before\s*download$' { $intNotificationLevel = 2 }
'^Notify\s*before\s*installation$' { $intNotificationLevel = 3 }
'^Scheduled\s*installation$' { $intNotificationLevel = 4 }
default { throw 'Invalid notification level'}
}
return $intNotificationLevel
}
hidden [void] SetWuaAuNotificationLevel() {
$this.ObjectHandler.settings.NotificationLevel = $this.GetWuaAuNotificationLevelInt()
$this.ObjectHandler.settings.Save()
}
hidden [void] AddWuaService() {
[string]$ServiceId = '7971f918-a847-4430-9279-4a52d1efe18d'
[int]$Flags = 7
[string]$AuthorizationCabPath = [string]::Empty
$this.ObjectHandler.ServiceManager.AddService2($ServiceId, $Flags, $AuthorizationCabPath)
}
hidden [void] RemoveWuaService() {
[string]$ServiceId = '7971f918-a847-4430-9279-4a52d1efe18d'
$this.ObjectHandler.ServiceManager.RemoveService($ServiceId)
}
[MaintenancePatching] Get() {
$this.InitializeObjectHandler()
$this.GetWuaWrapper({$this.GetWuaAuSettings()})
$this.GetWuaAuNotificationLevel()
$this.GetWuaWrapper({$this.GetWuaSession()})
$this.GetWuaSearcher()
$this.GetWuaServiceManager()
if($this.ObjectHandler.Search -and ($this.TestSearchResult()))
{
$this.PendingUpdates = $this.ObjectHandler.Search.Updates.Count
} else {
$this.PendingUpdates = 0
}
$this.UpdatesFound = [bool]$this.PendingUpdates
$this.notificationCompliant = (!$this.NotificationSettings -or $this.NotificationSettings -eq $this.CurrentUpdatesNotificationSetting)
$this.SourceCompliant = (!$this.UpdateSource -or $this.UpdateSource -eq $this.Source)
return $this
}
Set() {
$status = $this.Get()
if($status.PendingUpdates){
if($status.ObjectHandler.Search -and $status.ObjectHandler.Search.Updates.Count -gt 0) {
Write-Verbose -Verbose -Message 'Installing updates...'
#Write Results
$status.ObjectHandler.Search.Updates | ForEach-Object {
Write-Verbose -Message "Found update: $($_.Title)" -Verbose
}
$status.InvokeWuaDownloadUpdates()
$status.InvokeWuaInstallUpdates()
} else {
Write-Verbose -Verbose -Message 'No updates'
}
}
if(!$status.notificationCompliant) {
Try {
#TODO verify that group policy is not overriding this settings
# if it is throw an error, if it conflicts
$status.SetWuaAuNotificationLevel()
} Catch {
$ErrorMsg = $_.Exception.Message
Write-Verbose $ErrorMsg
}
}
if(!$status.SourceCompliant) {
if($status.UpdateSource -eq 'MicrosoftUpdate') {
Write-Verbose "Enable the Microsoft Update setting"
$status.AddWuaService()
Restart-Service wuauserv -ErrorAction SilentlyContinue
} elseif($status.UpdateSource -eq 'WindowsUpdate') {
Write-Verbose "Disable the Microsoft Update setting"
$status.RemoveWuaService()
}
}
}
[bool] Test() {
$status = $this.Get()
#TODO Add a modified version of Test-TargetResourceProperties here for verbose parameter validation
Write-Verbose "Updates Found: $($status.UpdatesFound)"
Write-Verbose "notifications compliant: $($status.notificationCompliant)"
Write-Verbose "service compliant: $($status.SourceCompliant)"
If(!$status.UpdatesFound -and $status.notificationCompliant -and $status.SourceCompliant) {
Write-Verbose 'No pending updates found.'
return $true
}
Else {
Write-Verbose 'Checking Window'
$window = ([MaintenanceWindow]$status.MaintenanceWindow).CheckWindow()
Write-Verbose "Window: $window"
return $window
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment