From e57cc1bcc179f3d19f6f2416bf7b43bf63fb92f8 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 16:30:47 +0000 Subject: [PATCH 01/12] UpdateServicesApprovalRule - add Assert-Module, more specific error handling, check post-install wizard has run, allow multiple products with same name --- .../DSC_UpdateServicesApprovalRule.psm1 | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 b/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 index 944138c..b9d3ced 100644 --- a/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 +++ b/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 @@ -46,16 +46,27 @@ function Get-TargetResource $Name ) + Assert-Module -ModuleName UpdateServices + try { $WsusServer = Get-WsusServer - $Ensure = 'Absent' - $Classifications = $null - $Products = $null - $ComputerGroups = $null - $Enabled = $null + } + catch + { + Write-Verbose -Message $script:localizedData.GetWsusServerFailed + } + + $Ensure = 'Absent' + $Classifications = $null + $Products = $null + $ComputerGroups = $null + $Enabled = $null - if ($null -ne $WsusServer) + try { + if (($null -ne $WsusServer) -and ` + (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup\Installed Role Services" ` + -Name 'UpdateServices-Services' -ErrorAction Stop).'UpdateServices-Services' -eq '2') { Write-Verbose -Message ('Identified WSUS server information: {0}' -f $WsusServer.Name) @@ -177,6 +188,8 @@ function Set-TargetResource $RunRuleNow ) + Assert-Module -ModuleName UpdateServices + try { if ($WsusServer = Get-WsusServer) @@ -220,11 +233,14 @@ function Set-TargetResource $ApprovalRule.Save() $ProductCollection = New-Object -TypeName Microsoft.UpdateServices.Administration.UpdateCategoryCollection + $AllWsusProducts = $WsusServer.GetUpdateCategories() foreach ($Product in $Products) { - if ($WsusProduct = Get-WsusProduct | Where-Object -FilterScript { $_.Product.Title -eq $Product }) + if ($WsusProduct = $AllWsusProducts | Where-Object -FilterScript { $_.Title -eq $Product }) { - $ProductCollection.Add($WsusServer.GetUpdateCategory($WsusProduct.Product.Id)) + $WsusProduct | Foreach-Object { + $ProductCollection.Add($_) + } } } @@ -262,7 +278,7 @@ function Set-TargetResource { New-InvalidOperationException -Message ( $script:localizedData.RuleFailedToCreate -f $Name - ) -ErrorRecord $_ + ) } } 'Absent' @@ -386,6 +402,8 @@ function Test-TargetResource $RunRuleNow ) + Assert-Module -ModuleName UpdateServices + $result = $true $ApprovalRule = Get-TargetResource -Name $Name From e13264f9dac650c2a5340a7898d2c501237f5972 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 16:31:04 +0000 Subject: [PATCH 02/12] UpdateServicesCleanup - add TimeOfDay test --- .../DSC_UpdateServicesCleanup.psm1 | 8 +++++++- .../en-US/DSC_UpdateServicesCleanup.strings.psd1 | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/source/DSCResources/DSC_UpdateServicesCleanup/DSC_UpdateServicesCleanup.psm1 b/source/DSCResources/DSC_UpdateServicesCleanup/DSC_UpdateServicesCleanup.psm1 index 383cb13..9523820 100644 --- a/source/DSCResources/DSC_UpdateServicesCleanup/DSC_UpdateServicesCleanup.psm1 +++ b/source/DSCResources/DSC_UpdateServicesCleanup/DSC_UpdateServicesCleanup.psm1 @@ -66,7 +66,7 @@ function Get-TargetResource } } } - $TimeOfDay = $Task.Triggers.StartBoundary.Split('T')[1] + $TimeOfDay = ([datetimeoffset]$Task.Triggers[0].StartBoundary).TimeOfDay.ToString('c') } else { @@ -363,6 +363,12 @@ function Test-TargetResource Write-Verbose -Message $script:localizedData.CleanupPublishedTestFailed $result = $false } + + if ($CleanupTask.TimeOfDay -ne $TimeOfDay) + { + Write-Verbose -Message $script:localizedData.TimeOfDayTestFailed + $result = $false + } } $result diff --git a/source/DSCResources/DSC_UpdateServicesCleanup/en-US/DSC_UpdateServicesCleanup.strings.psd1 b/source/DSCResources/DSC_UpdateServicesCleanup/en-US/DSC_UpdateServicesCleanup.strings.psd1 index 105bf10..2767c95 100644 --- a/source/DSCResources/DSC_UpdateServicesCleanup/en-US/DSC_UpdateServicesCleanup.strings.psd1 +++ b/source/DSCResources/DSC_UpdateServicesCleanup/en-US/DSC_UpdateServicesCleanup.strings.psd1 @@ -11,5 +11,6 @@ CompressTestFailed = Compress Updates test failed. CleanupObsoleteCptTestFailed= Cleanup Obsolete Computers test failed. CleanupContentTestFailed = Cleanup Unneeded Content Files test failed. CleanupPublishedTestFailed = Cleanup Local Published Content Files test failed. +TimeOfDayTestFailed = Time of Day test failed. TestFailedAfterSet = Test-TargetResource returned false after calling set. '@ From f974ef8a04750c3b03769d2ef685d9077295a1a9 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 16:31:51 +0000 Subject: [PATCH 03/12] UpdateServicesComputerTargetGroup - Assert-Module, First-Run Wizard check, better error handling --- ...DSC_UpdateServicesComputerTargetGroup.psm1 | 176 ++++++++++-------- 1 file changed, 95 insertions(+), 81 deletions(-) diff --git a/source/DSCResources/DSC_UpdateServicesComputerTargetGroup/DSC_UpdateServicesComputerTargetGroup.psm1 b/source/DSCResources/DSC_UpdateServicesComputerTargetGroup/DSC_UpdateServicesComputerTargetGroup.psm1 index 0b81ef5..b03053e 100644 --- a/source/DSCResources/DSC_UpdateServicesComputerTargetGroup/DSC_UpdateServicesComputerTargetGroup.psm1 +++ b/source/DSCResources/DSC_UpdateServicesComputerTargetGroup/DSC_UpdateServicesComputerTargetGroup.psm1 @@ -35,42 +35,52 @@ function Get-TargetResource $Path ) + Assert-Module -ModuleName UpdateServices + try { $WsusServer = Get-WsusServer } catch { - New-InvalidOperationException -Message $script:localizedData.WSUSConfigurationFailed -ErrorRecord $_ + Write-Verbose -Message $script:localizedData.GetWsusServerFailed } $Ensure = 'Absent' $Id = $null - if ($null -ne $WsusServer) - { - Write-Verbose -Message ($script:localizedData.GetWsusServerSucceeded -f $WsusServer.Name) - $ComputerTargetGroup = $WsusServer.GetComputerTargetGroups().Where({ $_.Name -eq $Name }) | Select-Object -First 1 - - if ($null -ne $ComputerTargetGroup) + try { + if (($null -ne $WsusServer) -and ` + (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup\Installed Role Services" ` + -Name 'UpdateServices-Services' -ErrorAction Stop).'UpdateServices-Services' -eq '2') { - $ComputerTargetGroupPath = Get-ComputerTargetGroupPath -ComputerTargetGroup $ComputerTargetGroup - if ($Path -eq $ComputerTargetGroupPath) - { - $Ensure = 'Present' - $Id = $ComputerTargetGroup.Id.Guid - Write-Verbose -Message ($script:localizedData.FoundComputerTargetGroup -f $Name, $Path, $Id) - } - else + Write-Verbose -Message ($script:localizedData.GetWsusServerSucceeded -f $WsusServer.Name) + $ComputerTargetGroup = $WsusServer.GetComputerTargetGroups().Where({ $_.Name -eq $Name }) | Select-Object -First 1 + + if ($null -ne $ComputerTargetGroup) { - # ComputerTargetGroup Names must be unique within the overall hierarchy - New-InvalidOperationException -Message ($script:localizedData.DuplicateComputerTargetGroup -f $ComputerTargetGroup.Name, $ComputerTargetGroupPath) + $ComputerTargetGroupPath = Get-ComputerTargetGroupPath -ComputerTargetGroup $ComputerTargetGroup + if ($Path -eq $ComputerTargetGroupPath) + { + $Ensure = 'Present' + $Id = $ComputerTargetGroup.Id.Guid + Write-Verbose -Message ($script:localizedData.FoundComputerTargetGroup -f $Name, $Path, $Id) + } + else + { + # ComputerTargetGroup Names must be unique within the overall hierarchy + New-InvalidOperationException -Message ($script:localizedData.DuplicateComputerTargetGroup -f $ComputerTargetGroup.Name, $ComputerTargetGroupPath) + } } } + else + { + Write-Verbose -Message $script:localizedData.GetWsusServerFailed + } } - else + catch { - Write-Verbose -Message $script:localizedData.GetWsusServerFailed + New-InvalidOperationException -Message $script:localizedData.WSUSConfigurationFailed -ErrorRecord $_ } if ($null -eq $Id) @@ -126,98 +136,100 @@ function Set-TargetResource $Path ) + Assert-Module -ModuleName UpdateServices + try { $WsusServer = Get-WsusServer - } - catch - { - New-InvalidOperationException -Message $script:localizedData.WSUSConfigurationFailed -ErrorRecord $_ - } - # break down path to identify the parent computer target group based on name and its own unique path - $ParentComputerTargetGroupName = (($Path -split '/')[-1]) - $ParentComputerTargetGroupPath = ($Path -replace "[/]$ParentComputerTargetGroupName", '') + # break down path to identify the parent computer target group based on name and its own unique path + $ParentComputerTargetGroupName = (($Path -split '/')[-1]) + $ParentComputerTargetGroupPath = ($Path -replace "[/]$ParentComputerTargetGroupName", '') - if ($null -ne $WsusServer) - { - $ParentComputerTargetGroups = $WsusServer.GetComputerTargetGroups().Where({ + if ($null -ne $WsusServer) + { + $ParentComputerTargetGroups = $WsusServer.GetComputerTargetGroups().Where({ $_.Name -eq $ParentComputerTargetGroupName }) | Select-Object -First 1 - if ($null -ne $ParentComputerTargetGroups) - { - foreach ($ParentComputerTargetGroup in $ParentComputerTargetGroups) + if ($null -ne $ParentComputerTargetGroups) { - $ComputerTargetGroupPath = Get-ComputerTargetGroupPath -ComputerTargetGroup $ParentComputerTargetGroup - if ($ParentComputerTargetGroupPath -eq $ComputerTargetGroupPath) + foreach ($ParentComputerTargetGroup in $ParentComputerTargetGroups) { - # parent Computer Target Group Exists - Write-Verbose -Message ($script:localizedData.FoundParentComputerTargetGroup -f $ParentComputerTargetGroupName, ` - $ParentComputerTargetGroupPath, $ParentComputerTargetGroup.Id.Guid) - - # create the new Computer Target Group if Ensure -eq 'Present' - if ($Ensure -eq 'Present') + $ComputerTargetGroupPath = Get-ComputerTargetGroupPath -ComputerTargetGroup $ParentComputerTargetGroup + if ($ParentComputerTargetGroupPath -eq $ComputerTargetGroupPath) { - try + # parent Computer Target Group Exists + Write-Verbose -Message ($script:localizedData.FoundParentComputerTargetGroup -f $ParentComputerTargetGroupName, ` + $ParentComputerTargetGroupPath, $ParentComputerTargetGroup.Id.Guid) + + # create the new Computer Target Group if Ensure -eq 'Present' + if ($Ensure -eq 'Present') { - $null = $WsusServer.CreateComputerTargetGroup($Name, $ParentComputerTargetGroup) - Write-Verbose -Message ($script:localizedData.CreateComputerTargetGroupSuccess -f $Name, $Path) - return + try + { + $null = $WsusServer.CreateComputerTargetGroup($Name, $ParentComputerTargetGroup) + Write-Verbose -Message ($script:localizedData.CreateComputerTargetGroupSuccess -f $Name, $Path) + return + } + catch + { + New-InvalidOperationException -Message ( + $script:localizedData.CreateComputerTargetGroupFailed -f $Name, $Path + ) -ErrorRecord $_ + } } - catch + else { - New-InvalidOperationException -Message ( - $script:localizedData.CreateComputerTargetGroupFailed -f $Name, $Path - ) -ErrorRecord $_ - } - } - else - { - # $Ensure -eq 'Absent' - must call the Delete() method on the group itself for removal - $ChildComputerTargetGroup = $ParentComputerTargetGroup.GetChildTargetGroups().Where({ + # $Ensure -eq 'Absent' - must call the Delete() method on the group itself for removal + $ChildComputerTargetGroup = $ParentComputerTargetGroup.GetChildTargetGroups().Where({ $_.Name -eq $Name }) | Select-Object -First 1 - if ($null -eq $ChildComputerTargetGroup) - { - # Already absent - Write-Verbose -Message ($script:localizedData.NotFoundComputerTargetGroup -f $Name, $Path) - return - } + if ($null -eq $ChildComputerTargetGroup) + { + # Already absent + Write-Verbose -Message ($script:localizedData.NotFoundComputerTargetGroup -f $Name, $Path) + return + } - try - { - $childId = $ChildComputerTargetGroup.Id.Guid - $null = $ChildComputerTargetGroup.Delete() - Write-Verbose -Message ($script:localizedData.DeleteComputerTargetGroupSuccess -f $Name, $childId, $Path) - return - } - catch - { - $childId = if ($ChildComputerTargetGroup) + try { - $ChildComputerTargetGroup.Id.Guid + $childId = $ChildComputerTargetGroup.Id.Guid + $null = $ChildComputerTargetGroup.Delete() + Write-Verbose -Message ($script:localizedData.DeleteComputerTargetGroupSuccess -f $Name, $childId, $Path) + return } - else + catch { - 'N/A' + $childId = if ($ChildComputerTargetGroup) + { + $ChildComputerTargetGroup.Id.Guid + } + else + { + 'N/A' + } + New-InvalidOperationException -Message ( + $script:localizedData.DeleteComputerTargetGroupFailed -f $Name, $childId, $Path + ) -ErrorRecord $_ } - New-InvalidOperationException -Message ( - $script:localizedData.DeleteComputerTargetGroupFailed -f $Name, $childId, $Path - ) -ErrorRecord $_ } } } } - } - New-InvalidOperationException -Message ($script:localizedData.NotFoundParentComputerTargetGroup -f $ParentComputerTargetGroupName, ` + New-InvalidOperationException -Message ($script:localizedData.NotFoundParentComputerTargetGroup -f $ParentComputerTargetGroupName, ` $ParentComputerTargetGroupPath, $Name) + } + else + { + Write-Verbose -Message $script:localizedData.GetWsusServerFailed + } } - else + catch { - Write-Verbose -Message $script:localizedData.GetWsusServerFailed + New-InvalidOperationException -Message $script:localizedData.WSUSConfigurationFailed -ErrorRecord $_ } } @@ -259,6 +271,8 @@ function Test-TargetResource $Path ) + Assert-Module -ModuleName UpdateServices + $result = Get-TargetResource -Name $Name -Path $Path if ($Ensure -eq $result.Ensure) From ed62b2df7dd1405ba82ea97c04763042bb8b0ccc Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 16:33:00 +0000 Subject: [PATCH 04/12] PDT.psm1 - do not return module start boolean alongside second return value --- source/Modules/PDT/PDT.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Modules/PDT/PDT.psm1 b/source/Modules/PDT/PDT.psm1 index 3748c98..4262acd 100644 --- a/source/Modules/PDT/PDT.psm1 +++ b/source/Modules/PDT/PDT.psm1 @@ -713,7 +713,7 @@ function Start-Win32Process { throw $err } - Wait-Win32ProcessStart @GetArguments + Wait-Win32ProcessStart @GetArguments | Out-Null } else { From 80001184ed4fbe434f72f9ccf73a3c5c580ca71c Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 16:33:17 +0000 Subject: [PATCH 05/12] UpdateServicesServer - many fixes including features --- .../DSC_UpdateServicesServer.psm1 | 1974 +++++++++++++---- .../DSC_UpdateServicesServer.schema.mof | 32 +- .../DSC_UpdateServicesServer.strings.psd1 | 114 +- 3 files changed, 1693 insertions(+), 427 deletions(-) diff --git a/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.psm1 b/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.psm1 index 3babfbc..ae98fd2 100644 --- a/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.psm1 +++ b/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.psm1 @@ -44,43 +44,60 @@ function Get-TargetResource $Ensure ) + Assert-Module -ModuleName UpdateServices + + $Ensure = 'Absent' + Write-Verbose -Message $script:localizedData.GettingWsusServer try { - if ($WsusServer = Get-WsusServer) + if (($WsusServer = Get-WsusServer) -and ` + (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup\Installed Role Services" ` + -Name 'UpdateServices-Services' -ErrorAction Stop).'UpdateServices-Services' -eq '2') { $Ensure = 'Present' } - else - { - $Ensure = 'Absent' - } } catch { - $Ensure = 'Absent' + Write-Verbose -Message $script:localizedData.GetWsusServerFailed } Write-Verbose -Message ($script:localizedData.WsusEnsureValue -f $Ensure) + if ($Ensure -eq 'Present') { Write-Verbose -Message $script:localizedData.GettingWsusConfig $WsusConfiguration = $WsusServer.GetConfiguration() + Write-Verbose -Message $script:localizedData.GettingWsusDatabaseConfig + $WsusDatabaseConfiguration = $WsusServer.GetDatabaseConfiguration() Write-Verbose -Message $script:localizedData.GettingWsusSubscription $WsusSubscription = $WsusServer.GetSubscription() + # Get the current time just before retrieving email configuration for StatusNotificationTimeOfDay DST workarounds + $currentDateTime = Get-Date + Write-Verbose -Message $script:localizedData.GettingWsusEmailNotificationConfig + $WsusEmailNotificationConfiguration = $WsusServer.GetEmailNotificationConfiguration() Write-Verbose -Message $script:localizedData.GettingWsusSQLServer - $SQLServer = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup' ` - -Name 'SQLServerName').SQLServerName - Write-Verbose -Message ($script:localizedData.SQLServerName -f $SQLServer) - Write-Verbose -Message $script:localizedData.GetWSUSContentDir - $ContentDir = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup' ` - -Name 'ContentDir').ContentDir - Write-Verbose -Message ($script:localizedData.WsusContentDir -f $ContentDir) - - Write-Verbose -Message $script:localizedData.GetWsusImproveProgram - $UpdateImprovementProgram = $WsusConfiguration.MURollupOptin - Write-Verbose -Message ($script:localizedData.ImprovementProgram -f $UpdateImprovementProgram) + if (-not $WsusDatabaseConfiguration.IsUsingWindowsInternalDatabase) + { + $SQLServer = $WsusDatabaseConfiguration.ServerName + Write-Verbose -Message ($script:localizedData.SQLServerName -f $SQLServer) + } + else { + $SQLServer = '' + } + + if (-not $WsusConfiguration.IsReplicaServer) + { + Write-Verbose -Message $script:localizedData.GetWsusImproveProgram + $UpdateImprovementProgram = $WsusConfiguration.MURollupOptin + Write-Verbose -Message ($script:localizedData.ImprovementProgram -f $UpdateImprovementProgram) + } + else + { + $UpdateImprovementProgram = $null + } if (-not $WsusConfiguration.SyncFromMicrosoftUpdate) { @@ -88,38 +105,87 @@ function Get-TargetResource $UpstreamServerName = $WsusConfiguration.UpstreamWsusServerName $UpstreamServerPort = $WsusConfiguration.UpstreamWsusServerPortNumber $UpstreamServerSSL = $WsusConfiguration.UpstreamWsusServerUseSsl - $UpstreamServerReplica = $WsusConfiguration.IsReplicaServer Write-Verbose -Message ($script:localizedData.UpstreamServer -f ` - $UpstreamServerName, $UpstreamServerPort, $UpstreamServerSSL, $UpstreamServerReplica) + $UpstreamServerName, $UpstreamServerPort, $UpstreamServerSSL) } else { $UpstreamServerName = '' - $UpstreamServerPort = $null + $UpstreamServerPort = 0 $UpstreamServerSSL = $null - $UpstreamServerReplica = $null } + Write-Verbose -Message $script:localizedData.GetReplicaServer + if (-not $WsusConfiguration.SyncFromMicrosoftUpdate) + { + $UpstreamServerReplica = $WsusConfiguration.IsReplicaServer + } + else + { + $UpstreamServerReplica = $false + } + Write-Verbose -Message ($script:localizedData.ReplicaServer -f $UpstreamServerReplica) + if ($WsusConfiguration.UseProxy) { Write-Verbose -Message $script:localizedData.GetWsusProxyServer $ProxyServerName = $WsusConfiguration.ProxyName $ProxyServerPort = $WsusConfiguration.ProxyServerPort - $ProxyServerBasicAuthentication = $WsusConfiguration.AllowProxyCredentialsOverNonSsl if (-not ($WsusConfiguration.AnonymousProxyAccess)) { - $ProxyServerCredentialUsername = "$($WsusConfiguration.ProxyUserDomain)\ ` - $($WsusConfiguration.ProxyUserName)".Trim('\') + if ($WsusConfiguration.ProxyUserDomain) + { + $ProxyServerCredentialUsername = "$($WsusConfiguration.ProxyUserDomain)\$($WsusConfiguration.ProxyUserName)" + } + else + { + $ProxyServerCredentialUsername = $WsusConfiguration.ProxyUserName + } + $ProxyServerBasicAuthentication = $WsusConfiguration.AllowProxyCredentialsOverNonSsl + } + else + { + $ProxyServerCredentialUsername = '' + $ProxyServerBasicAuthentication = $null } - Write-Verbose -Message ($script:localizedData.WsusProxyServer -f $ProxyServerName, $ProxyServerPort, $ProxyServerBasicAuthentication) + Write-Verbose -Message ($script:localizedData.WsusProxyServer -f $ProxyServerName, $ProxyServerPort, ` + $ProxyServerCredentialUsername, $ProxyServerBasicAuthentication) } else { $ProxyServerName = '' - $ProxyServerPort = $null + $ProxyServerPort = 0 + $ProxyServerCredentialUsername = '' $ProxyServerBasicAuthentication = $null } + Write-Verbose -Message $script:localizedData.GettingWsusUpdateFiles + if (-not $WsusConfiguration.HostBinariesOnMicrosoftUpdate) + { + $ContentDir = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup' ` + -Name 'ContentDir').ContentDir + $DownloadUpdateBinariesAsNeeded = $WsusConfiguration.DownloadUpdateBinariesAsNeeded + $DownloadExpressPackages = $WsusConfiguration.DownloadExpressPackages + if (-not $WsusConfiguration.SyncFromMicrosoftUpdate) + { + $GetContentFromMU = $WsusConfiguration.GetContentFromMU + } + else + { + $GetContentFromMU = $null + } + } + else + { + $ContentDir = '' + $DownloadUpdateBinariesAsNeeded = $null + $DownloadExpressPackages = $null + $GetContentFromMU = $null + } + Write-Verbose -Message ($script:localizedData.WsusUpdateFiles -f $ContentDir, $DownloadUpdateBinariesAsNeeded, ` + $DownloadExpressPackages, $GetContentFromMU) + + # Get languages - even for servers that host binaries on Microsoft Update, as it is relevant to server configuration Write-Verbose -Message $script:localizedData.GettingWsusLanguage if ($WsusConfiguration.AllUpdateLanguagesEnabled) { @@ -127,15 +193,16 @@ function Get-TargetResource } else { - $Languages = ($WsusConfiguration.GetEnabledUpdateLanguages()) -join ',' + $Languages = [String[]]$WsusConfiguration.GetEnabledUpdateLanguages() } + Write-Verbose -Message ($script:localizedData.WsusLanguages -f ($Languages -join ',')) - Write-Verbose -Message ($script:localizedData.WsusLanguages -f $Languages) + # Get classifications - even for replica servers where these are read only, as it is relevant to server configuration Write-Verbose -Message $script:localizedData.GettingWsusClassifications if ($Classifications = @($WsusSubscription.GetUpdateClassifications().ID.Guid)) { if ($null -eq (Compare-Object -ReferenceObject ($Classifications | Sort-Object -Unique) -DifferenceObject ` - (($WsusServer.GetUpdateClassifications().ID.Guid) | Sort-Object -Unique) -SyncWindow 0)) + (($WsusServer.GetUpdateClassifications().ID.Guid) | Sort-Object -Unique) -SyncWindow 0)) { $Classifications = @('*') } @@ -144,13 +211,14 @@ function Get-TargetResource { $Classifications = @('*') } - Write-Verbose -Message ($script:localizedData.WsusClassifications -f $Classifications) + + # Get products - even for replica servers where these are read only, as it is relevant to server configuration Write-Verbose -Message $script:localizedData.GettingWsusProducts if ($Products = @($WsusSubscription.GetUpdateCategories().Title) | Sort-Object -Unique) { if ($null -eq (Compare-Object -ReferenceObject $Products -DifferenceObject ` - (($WsusServer.GetUpdateCategories().Title) | Sort-Object -Unique) -SyncWindow 0)) + (($WsusServer.GetUpdateCategories().Title) | Sort-Object -Unique) -SyncWindow 0)) { $Products = @('*') } @@ -159,17 +227,155 @@ function Get-TargetResource { $Products = @('*') } - Write-Verbose -Message ($script:localizedData.WsusProducts -f $($Products -join '; ')) + + if (-not $WsusConfiguration.IsReplicaServer) + { + Write-Verbose -Message $script:localizedData.GettingWsusAdvancedAutomaticApprovals + $AutoApproveWsusInfrastructureUpdates = $WsusConfiguration.AutoApproveWsusInfrastructureUpdates + Write-Verbose -Message ($script:localizedData.WsusAutoApproveWsusInfrastructureUpdates -f $AutoApproveWsusInfrastructureUpdates) + $AutoRefreshUpdateApprovals = $WsusConfiguration.AutoRefreshUpdateApprovals + Write-Verbose -Message ($script:localizedData.WsusAutoRefreshUpdateApprovals -f $AutoRefreshUpdateApprovals) + if ($WsusConfiguration.AutoRefreshUpdateApprovals) + { + $AutoRefreshUpdateApprovalsDeclineExpired = $WsusConfiguration.AutoRefreshUpdateApprovalsDeclineExpired + Write-Verbose -Message ($script:localizedData.WsusAutoRefreshUpdateApprovalsDeclineExpired -f $AutoRefreshUpdateApprovalsDeclineExpired) + } + else + { + $AutoRefreshUpdateApprovalsDeclineExpired = $null + } + } + else + { + $AutoApproveWsusInfrastructureUpdates = $null + $AutoRefreshUpdateApprovals = $null + $AutoRefreshUpdateApprovalsDeclineExpired = $null + } + Write-Verbose -Message $script:localizedData.GettingWsusSyncConfig $SynchronizeAutomatically = $WsusSubscription.SynchronizeAutomatically Write-Verbose -Message ($script:localizedData.WsusSyncAuto -f $SynchronizeAutomatically) - $SynchronizeAutomaticallyTimeOfDay = $WsusSubscription.SynchronizeAutomaticallyTimeOfDay - Write-Verbose -Message ($script:localizedData.WsusSyncAutoTimeOfDay -f $SynchronizeAutomaticallyTimeOfDay ) - $SynchronizationsPerDay = $WsusSubscription.NumberOfSynchronizationsPerDay - Write-Verbose -Message ($script:localizedData.WsusSyncPerDay -f $SynchronizationsPerDay) + if ($WsusSubscription.SynchronizeAutomatically) + { + $SynchronizeAutomaticallyTimeOfDay = $WsusSubscription.SynchronizeAutomaticallyTimeOfDay + Write-Verbose -Message ($script:localizedData.WsusSyncAutoTimeOfDay -f $SynchronizeAutomaticallyTimeOfDay ) + $SynchronizationsPerDay = $WsusSubscription.NumberOfSynchronizationsPerDay + Write-Verbose -Message ($script:localizedData.WsusSyncPerDay -f $SynchronizationsPerDay) + } + else + { + $SynchronizeAutomaticallyTimeOfDay = '' + $SynchronizationsPerDay = 0 + } + + Write-Verbose -Message $script:localizedData.GettingWsusTargetingMode $ClientTargetingMode = $WsusConfiguration.TargetingMode Write-Verbose -Message ($script:localizedData.WsusClientTargetingMode -f $ClientTargetingMode) + + if (-not $WsusConfiguration.IsReplicaServer) + { + Write-Verbose -Message $script:localizedData.GettingWsusReportingRollup + $DoDetailedRollup = $WsusConfiguration.DoDetailedRollup + Write-Verbose -Message ($script:localizedData.WsusDoDetailedRollup -f $DoDetailedRollup) + } + else + { + $DoDetailedRollup = $null + } + + Write-Verbose -Message $script:localizedData.GettingWsusEmailNotifications + if ($WsusEmailNotificationConfiguration.SendSyncNotification) + { + # Wrapped in @() to return array even if only one object is returned + $SyncNotificationRecipients = @($WsusEmailNotificationConfiguration.SyncNotificationRecipients | + Select-Object -ExpandProperty Address) + Write-Verbose -Message ($script:localizedData.WsusSyncNotification -f $($SyncNotificationRecipients -join ',')) + } + else { + $SyncNotificationRecipients = @() + } + + if ($WsusEmailNotificationConfiguration.SendStatusNotification) + { + $StatusNotificationFrequency = $WsusEmailNotificationConfiguration.StatusNotificationFrequency + $StatusNotificationTimeOfDay = $WsusEmailNotificationConfiguration.StatusNotificationTimeOfDay + + # When Daylight Savings Time is in effect, StatusNotificationTimeOfDay is supplied as UTC with the DST offset deducted + # Must add the DST offset after retrieving to get the actual time - see https://learn.microsoft.com/en-us/previous-versions/windows/desktop/aa351886(v=vs.85) + if ($currentDateTime.IsDaylightSavingTime()) + { + $currentTimeZone = Get-TimeZone + + # Convert StatusNotificationTimeOfDay from a Timespan to a DateTimeOffset value defined in UTC + $StatusNotificationTimeOfDayDateTimeOffset = [datetimeoffset]"$($StatusNotificationTimeOfDay.ToString('c'))Z" + + # Add the currently active DST offset to the retrieved DateTimeOffset to get UTC TimeOfDay as TimeSpan + $StatusNotificationTimeOfDay = $StatusNotificationTimeOfDayDateTimeOffset + ([datetimeoffset]$currentDateTime).Offset - $currentTimeZone.BaseUtcOffset | Select-Object -ExpandProperty TimeOfDay + } + # Wrapped in @() to return array even if only one object is returned + $StatusNotificationRecipients = @($WsusEmailNotificationConfiguration.StatusNotificationRecipients | + Select-Object -ExpandProperty Address) + Write-Verbose -Message ($script:localizedData.WsusStatusNotification -f $StatusNotificationFrequency, ` + $StatusNotificationTimeOfDay, $($StatusNotificationRecipients -join ',')) + } + else { + $StatusNotificationFrequency = '' + $StatusNotificationTimeOfDay = '' + $StatusNotificationRecipients = @() + } + + $EmailLanguage = $WsusEmailNotificationConfiguration.EmailLanguage + Write-Verbose -Message ($script:localizedData.WsusEmailLanguage -f $EmailLanguage) + $SmtpHostName = $WsusEmailNotificationConfiguration.SmtpHostName + Write-Verbose -Message ($script:localizedData.WsusSmtpHostName -f $SmtpHostName) + if ($WsusEmailNotificationConfiguration.SmtpHostName) + { + $SmtpPort = $WsusEmailNotificationConfiguration.SmtpPort + Write-Verbose -Message ($script:localizedData.WsusSmtpPort -f $SmtpPort) + } + else + { + $SmtpPort = 0 + } + $SenderDisplayName = $WsusEmailNotificationConfiguration.SenderDisplayName + Write-Verbose -Message ($script:localizedData.WsusSenderDisplayName -f $SenderDisplayName) + $SenderEmailAddress = $WsusEmailNotificationConfiguration.SenderEmailAddress + Write-Verbose -Message ($script:localizedData.WsusSenderEmailAddress -f $SenderEmailAddress) + + if ($WsusEmailNotificationConfiguration.SmtpHostName) + { + if ($WsusEmailNotificationConfiguration.SmtpServerRequiresAuthentication) + { + $SmtpUserName = $WsusEmailNotificationConfiguration.SmtpUserName + Write-Verbose -Message ($script:localizedData.WsusSmtpServerUserName -f $SmtpUserName) + } + else + { + $SmtpUserName = '' + } + } + else + { + $SmtpUserName = '' + } + + Write-Verbose -Message $script:localizedData.GettingWsusIIsDynamicCompression + $IIsDynamicCompression = ($null -ne ((Get-Item -Path 'HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup' ` + -ErrorAction SilentlyContinue) | Where-Object -Property Property -Contains 'IIsDynamicCompression')) + Write-Verbose -Message ($script:localizedData.IIsDynamicCompression -f $IIsDynamicCompression) + + Write-Verbose -Message $script:localizedData.GettingWsusBitsDownloadPriorityForeground + $BitsDownloadPriorityForeground = $WsusConfiguration.BitsDownloadPriorityForeground + Write-Verbose -Message ($script:localizedData.BitsDownloadPriorityForeground -f $BitsDownloadPriorityForeground) + + Write-Verbose -Message $script:localizedData.GettingWsusLocalPublishingMaxCabSize + $LocalPublishingMaxCabSize = $WsusConfiguration.LocalPublishingMaxCabSize + Write-Verbose -Message ($script:localizedData.WsusLocalPublishingMaxCabSize -f $LocalPublishingMaxCabSize) + + Write-Verbose -Message $script:localizedData.GettingWsusMaxSimultaneousFileDownloads + $MaxSimultaneousFileDownloads = $WsusConfiguration.MaxSimultaneousFileDownloads + Write-Verbose -Message ($script:localizedData.WsusMaxSimultaneousFileDownloads -f $MaxSimultaneousFileDownloads) } $returnValue = @{ @@ -185,13 +391,34 @@ function Get-TargetResource ProxyServerPort = $ProxyServerPort ProxyServerCredentialUsername = $ProxyServerCredentialUsername ProxyServerBasicAuthentication = $ProxyServerBasicAuthentication + DownloadUpdateBinariesAsNeeded = $DownloadUpdateBinariesAsNeeded + DownloadExpressPackages = $DownloadExpressPackages + GetContentFromMU = $GetContentFromMU Languages = $Languages Products = $Products Classifications = $Classifications SynchronizeAutomatically = $SynchronizeAutomatically SynchronizeAutomaticallyTimeOfDay = $SynchronizeAutomaticallyTimeOfDay SynchronizationsPerDay = $SynchronizationsPerDay + AutoApproveWsusInfrastructureUpdates = $AutoApproveWsusInfrastructureUpdates + AutoRefreshUpdateApprovals = $AutoRefreshUpdateApprovals + AutoRefreshUpdateApprovalsDeclineExpired = $AutoRefreshUpdateApprovalsDeclineExpired ClientTargetingMode = $ClientTargetingMode + DoDetailedRollup = $DoDetailedRollup + SyncNotificationRecipients = $SyncNotificationRecipients + StatusNotificationFrequency = $StatusNotificationFrequency + StatusNotificationTimeOfDay = $StatusNotificationTimeOfDay + StatusNotificationRecipients = $StatusNotificationRecipients + EmailLanguage = $EmailLanguage + SmtpHostName = $SmtpHostName + SmtpPort = $SmtpPort + SenderDisplayName = $SenderDisplayName + SenderEmailAddress = [String]$SenderEmailAddress + SmtpUserName = $SmtpUserName + IIsDynamicCompression = $IIsDynamicCompression + BitsDownloadPriorityForeground = $BitsDownloadPriorityForeground + LocalPublishingMaxCabSize = $LocalPublishingMaxCabSize + MaxSimultaneousFileDownloads = $MaxSimultaneousFileDownloads } $returnValue @@ -213,7 +440,8 @@ function Get-TargetResource Optionally specify a SQL instance to store WSUS data .PARAMETER ContentDir - Location to store WSUS content files + Location to store WSUS content files. + Set as empty string ('') to download from Microsoft Update. .PARAMETER UpdateImprovementProgram Provide feedback to Microsoft to help improve WSUS @@ -242,6 +470,15 @@ function Get-TargetResource .PARAMETER ProxyServerBasicAuthentication Use basic auth for proxy + .PARAMETER DownloadUpdateBinariesAsNeeded + Updates are downloaded only when they are approved + + .PARAMETER DownloadExpressPackages + Express installation packages should be downloaded + + .PARAMETER GetContentFromMU + Update binaries are downloaded from Microsoft Update instead of from the upstream server + .PARAMETER Languages Specify list of languages for content, or '*' for all @@ -255,7 +492,9 @@ function Get-TargetResource Automatically synchronize the WSUS instance .PARAMETER SynchronizeAutomaticallyTimeOfDay - Time of day to schedule an automatic synchronization + Time of day to schedule an automatic synchronization (as UTC) + The value must be a string representation of a TimeSpan value + The valid range is 00:00:00 to 23:59:59 inclusive .PARAMETER SynchronizationsPerDay Number of automatic synchronizations per day @@ -263,10 +502,70 @@ function Get-TargetResource .PARAMETER Synchronize Run a synchronization immediately when running Set + .PARAMETER AutoApproveWsusInfrastructureUpdates + WSUS infrastructure updates are approved automatically + + .PARAMETER AutoRefreshUpdateApprovals + The latest revision of an update should be approved automatically + + .PARAMETER AutoRefreshUpdateApprovalsDeclineExpired + An update should be automatically declined when it is revised to be expired and + AutoRefreshUpdateApprovals is enabled + .PARAMETER ClientTargetingMode - An enumerated value that describes if how the Target Groups are populated. + An enumerated value that describes how the Target Groups are populated. Accepts 'Client'(default) or 'Server'. + .PARAMETER DoDetailedRollup + The downstream server should roll up detailed computer and update status information + + .PARAMETER SyncNotificationRecipients + E-mail addresses of those to whom notification of new updates should be sent, omit for no notifications + + .PARAMETER StatusNotificationFrequency + The frequency with which e-mail notifications should be sent + Accepts 'Daily'(default) or 'Weekly' + + .PARAMETER StatusNotificationTimeOfDay + The time of the day e-mail notifications should be sent (as UTC) + The value must be a string representation of a TimeSpan value + The valid range is 00:00:00 to 23:59:59 inclusive + + .PARAMETER StatusNotificationRecipients + E-mail addresses of those to whom update status notification should be sent, omit for no notifications + + .PARAMETER EmailLanguage + E-mail language setting + + .PARAMETER SmtpHostName + The host name of the SMTP server + + .PARAMETER SmtpPort + The port number of the SMTP server + + .PARAMETER SenderDisplayName + The display name of the e-mail sender + + .PARAMETER SenderEmailAddress + The e-mail address of the sender + + .PARAMETER EmailServerCredential + The e-mail server credential, omit for anonymous. + + .PARAMETER IIsDynamicCompression + Use Xpress Encoding to compress update metadata. + Results in significant bandwidth savings, at the expense of some CPU overhead. + + .PARAMETER BitsDownloadPriorityForeground + Use foreground priority for BITS downloads to handle issues with proxy servers that do not correctly handle + HTTP 1.1 range request. + + .PARAMETER LocalPublishingMaxCabSize + The maximum .cab file size (in megabytes) that Local Publishing will create + + .PARAMETER MaxSimultaneousFileDownloads + The maximum number of concurrent update downloads + #> function Set-TargetResource { @@ -288,7 +587,7 @@ function Set-TargetResource [Parameter()] [System.String] - $ContentDir = '%SystemDrive%\WSUS', + $ContentDir, [Parameter()] [System.Boolean] @@ -304,7 +603,7 @@ function Set-TargetResource [Parameter()] [System.Boolean] - $UpstreamServerSSL, + $UpstreamServerSSL = $false, [Parameter()] [System.Boolean] @@ -324,29 +623,45 @@ function Set-TargetResource [Parameter()] [System.Boolean] - $ProxyServerBasicAuthentication, + $ProxyServerBasicAuthentication = $false, + + [Parameter()] + [System.Boolean] + $DownloadUpdateBinariesAsNeeded, + + [Parameter()] + [System.Boolean] + $DownloadExpressPackages, + + [Parameter()] + [System.Boolean] + $GetContentFromMU, [Parameter()] [System.String[]] - $Languages = @('*'), + $Languages, [Parameter()] [System.String[]] - $Products = @('Windows', 'Office'), + $Products, [Parameter()] [System.String[]] - $Classifications = @('E6CF1350-C01B-414D-A61F-263D14D133B4', 'E0789628-CE08-4437-BE74-2495B842F43B', '0FA1201D-4330-4FA8-8AE9-B877473B6441'), + $Classifications, [Parameter()] [System.Boolean] $SynchronizeAutomatically, [Parameter()] + [ValidateScript({ + ([ValidateRange(0, 86399)]$valueInSeconds = [TimeSpan]::Parse($_).TotalSeconds); $? + })] [System.String] $SynchronizeAutomaticallyTimeOfDay, [Parameter()] + [ValidateRange(1, 24)] [System.UInt16] $SynchronizationsPerDay = 1, @@ -354,19 +669,103 @@ function Set-TargetResource [System.Boolean] $Synchronize, + [Parameter()] + [System.Boolean] + $AutoApproveWsusInfrastructureUpdates, + + [Parameter()] + [System.Boolean] + $AutoRefreshUpdateApprovals, + + [Parameter()] + [System.Boolean] + $AutoRefreshUpdateApprovalsDeclineExpired, + [Parameter()] [ValidateSet('Client', 'Server')] [System.String] - $ClientTargetingMode + $ClientTargetingMode, + + [Parameter()] + [System.Boolean] + $DoDetailedRollup, + + [Parameter()] + [System.String[]] + $SyncNotificationRecipients, + + [Parameter()] + [ValidateSet('Daily', 'Weekly')] + [System.String] + $StatusNotificationFrequency, + + [Parameter()] + [ValidateScript({ + ([ValidateRange(0, 86399)]$valueInSeconds = [TimeSpan]::Parse($_).TotalSeconds); $? + })] + [System.String] + $StatusNotificationTimeOfDay, + + [Parameter()] + [System.String[]] + $StatusNotificationRecipients, + + [Parameter()] + [System.String] + $EmailLanguage, + + [Parameter()] + [System.String] + $SmtpHostName, + + [Parameter()] + [System.UInt16] + $SmtpPort = 25, + + [Parameter()] + [System.String] + $SenderDisplayName, + + [Parameter()] + [System.String] + $SenderEmailAddress, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EmailServerCredential, + + [Parameter()] + [System.Boolean] + $IIsDynamicCompression, + + [Parameter()] + [System.Boolean] + $BitsDownloadPriorityForeground, + + [Parameter()] + [System.UInt32] + $LocalPublishingMaxCabSize, + + [Parameter()] + [System.UInt32] + $MaxSimultaneousFileDownloads ) - # Is WSUS configured? + Assert-Module -ModuleName UpdateServices + + # Check whether the post installation tasks for the WSUS Services role still need to be run try { - if ($WsusServer = Get-WsusServer) + if (($WsusServer = Get-WsusServer) -and ` + (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup\Installed Role Services" ` + -Name 'UpdateServices-Services' -ErrorAction Stop).'UpdateServices-Services' -eq '2') { $PostInstall = $false } + else + { + $PostInstall = $true + } } catch { @@ -384,25 +783,31 @@ function Set-TargetResource $Path = Invoke-ResolvePath $Path Write-Verbose -Message ($script:localizedData.ResolveWsusUtilExePath -f $Path) - $Arguments = 'postinstall ' + $Arguments = 'postinstall' if ($PSBoundParameters.ContainsKey('SQLServer')) { - $Arguments += "SQL_INSTANCE_NAME=$SQLServer " + $Arguments += " SQL_INSTANCE_NAME=$SQLServer" + } + if ($PSBoundParameters.ContainsKey('ContentDir')) + { + if ($ContentDir) + { + $Arguments += " CONTENT_DIR=$([Environment]::ExpandEnvironmentVariables($ContentDir))" + } } - $Arguments += "CONTENT_DIR=$([Environment]::ExpandEnvironmentVariables($ContentDir))" Write-Verbose -Message ($script:localizedData.WsusUtilArgs -f $Arguments) if ($SetupCredential) { $Process = Start-Win32Process -Path $Path -Arguments $Arguments -Credential $SetupCredential - Write-Verbose -Message [string]$Process + Write-Verbose -Message $Process Wait-Win32ProcessEnd -Path $Path -Arguments $Arguments } else { $Process = Start-Win32Process -Path $Path -Arguments $Arguments - Write-Verbose -Message [string]$Process + Write-Verbose -Message $Process Wait-Win32ProcessEnd -Path $Path -Arguments $Arguments } } @@ -430,67 +835,166 @@ function Set-TargetResource # Get configuration and make sure that the configuration can be saved before continuing $WsusConfiguration = $WsusServer.GetConfiguration() $WsusSubscription = $WsusServer.GetSubscription() + $WsusEmailNotificationConfiguration = $WsusServer.GetEmailNotificationConfiguration() Write-Verbose -Message $script:localizedData.CheckPreviousConfig - SaveWsusConfiguration + Save-WsusConfiguration - # Configure Update Improvement Program - Write-Verbose -Message $script:localizedData.ConfiguringUpdateImprove - $WsusConfiguration.MURollupOptin = $UpdateImprovementProgram + # If this is not a replica server + if (-not $UpstreamServerReplica) + { + # Configure Update Improvement Program + if ($PSBoundParameters.ContainsKey('UpdateImprovementProgram')) + { + Write-Verbose -Message $script:localizedData.ConfiguringUpdateImprove + $WsusConfiguration.MURollupOptin = $UpdateImprovementProgram + } + } # Configure Upstream Server if ($PSBoundParameters.ContainsKey('UpstreamServerName')) { - Write-Verbose -Message $script:localizedData.ConfiguringUpstreamServer - $WsusConfiguration.SyncFromMicrosoftUpdate = $false - $WsusConfiguration.UpstreamWsusServerName = $UpstreamServerName - $WsusConfiguration.UpstreamWsusServerPortNumber = $UpstreamServerPort - $WsusConfiguration.UpstreamWsusServerUseSsl = $UpstreamServerSSL - $WsusConfiguration.IsReplicaServer = $UpstreamServerReplica + if ($UpstreamServerName) + { + Write-Verbose -Message $script:localizedData.ConfiguringUpstreamServer + $WsusConfiguration.SyncFromMicrosoftUpdate = $false + $WsusConfiguration.UpstreamWsusServerName = $UpstreamServerName + $WsusConfiguration.UpstreamWsusServerPortNumber = $UpstreamServerPort + $WsusConfiguration.UpstreamWsusServerUseSsl = $UpstreamServerSSL + } + else + { + Write-Verbose -Message $script:localizedData.ConfiguringWsusMsftUpdates + $WsusConfiguration.SyncFromMicrosoftUpdate = $true + } } - else + + # Configure Upstream Server Replica separately as IsReplicaServer=$true prevents other settings even when SyncFromMicrosoftUpdate=$true + if ($PSBoundParameters.ContainsKey('UpstreamServerReplica')) { - Write-Verbose -Message $script:localizedData.ConfiguringWsusMsftUpdates - $WsusConfiguration.SyncFromMicrosoftUpdate = $true + if ($UpstreamServerName) + { + Write-Verbose -Message $script:localizedData.ConfiguringUpstreamServerReplica + $WsusConfiguration.IsReplicaServer = $UpstreamServerReplica + } + else + { + if (-not $UpstreamServerReplica) # If no upstream server is configured, only set this if it is $false + { + Write-Verbose -Message $script:localizedData.ConfiguringUpstreamServerReplica + $WsusConfiguration.IsReplicaServer = $UpstreamServerReplica + } + } } # Configure Proxy Server if ($PSBoundParameters.ContainsKey('ProxyServerName')) { - Write-Verbose -Message $script:localizedData.ConfiguringWsusProxy - $WsusConfiguration.UseProxy = $true - $WsusConfiguration.ProxyName = $ProxyServerName - $WsusConfiguration.ProxyServerPort = $ProxyServerPort - if ($PSBoundParameters.ContainsKey('ProxyServerCredential')) + if ($ProxyServerName) { - Write-Verbose -Message $script:localizedData.ConfiguringProxyCred - $WsusConfiguration.ProxyUserDomain = $ProxyServerCredential.GetNetworkCredential().Domain - $WsusConfiguration.ProxyUserName = $ProxyServerCredential.GetNetworkCredential().UserName - $WsusConfiguration.SetProxyPassword($ProxyServerCredential.GetNetworkCredential().Password) - $WsusConfiguration.AllowProxyCredentialsOverNonSsl = $ProxyServerBasicAuthentication - $WsusConfiguration.AnonymousProxyAccess = $false + Write-Verbose -Message $script:localizedData.ConfiguringWsusProxy + $WsusConfiguration.UseProxy = $true + $WsusConfiguration.ProxyName = $ProxyServerName + $WsusConfiguration.ProxyServerPort = $ProxyServerPort + if ($PSBoundParameters.ContainsKey('ProxyServerCredential')) + { + if ($ProxyServerCredential) + { + Write-Verbose -Message $script:localizedData.ConfiguringProxyCred + $WsusConfiguration.AnonymousProxyAccess = $false + $WsusConfiguration.ProxyUserDomain = $ProxyServerCredential.GetNetworkCredential().Domain + $WsusConfiguration.ProxyUserName = $ProxyServerCredential.GetNetworkCredential().UserName + $WsusConfiguration.SetProxyPassword($ProxyServerCredential.GetNetworkCredential().Password) + if ($PSBoundParameters.ContainsKey('ProxyServerBasicAuthentication')) + { + $WsusConfiguration.AllowProxyCredentialsOverNonSsl = $ProxyServerBasicAuthentication + } + } + else + { + Write-Verbose -Message $script:localizedData.RemovingProxyCred + $WsusConfiguration.AnonymousProxyAccess = $true + } + } } else { - Write-Verbose -Message $script:localizedData.RemovingProxyCred - $WsusConfiguration.AnonymousProxyAccess = $true + Write-Verbose -Message $script:localizedData.ConfiguringNoProxy + $WsusConfiguration.UseProxy = $false } } - else - { - Write-Verbose -Message $script:localizedData.ConfiguringNoProxy - $WsusConfiguration.UseProxy = $false - } - #Languages - Write-Verbose -Message $script:localizedData.ConfiguringLanguages - if ($Languages -eq '*') + + # Configure Update Files + if ($PSBoundParameters.ContainsKey('ContentDir')) { - $WsusConfiguration.AllUpdateLanguagesEnabled = $true + if ($ContentDir) + { + Write-Verbose -Message $script:localizedData.ConfiguringUpdateFiles + $WsusConfiguration.HostBinariesOnMicrosoftUpdate = $false + if ($PSBoundParameters.ContainsKey('DownloadUpdateBinariesAsNeeded')) + { + Write-Verbose -Message $script:localizedData.ConfiguringDownloadUpdateBinariesAsNeeded + $WsusConfiguration.DownloadUpdateBinariesAsNeeded = $DownloadUpdateBinariesAsNeeded + } + if ($PSBoundParameters.ContainsKey('DownloadExpressPackages')) + { + Write-Verbose -Message $script:localizedData.ConfiguringDownloadExpressPackages + $WsusConfiguration.DownloadExpressPackages = $DownloadExpressPackages + } + # If we have an upstream server configured - otherwise no point configuring this + if ($UpstreamServerName) + { + if ($PSBoundParameters.ContainsKey('GetContentFromMU')) + { + Write-Verbose -Message $script:localizedData.ConfiguringGetContentFromMU + $WsusConfiguration.GetContentFromMU = $GetContentFromMU + } + } + + # Languages + if ($PSBoundParameters.ContainsKey('Languages')) + { + Write-Verbose -Message $script:localizedData.ConfiguringLanguages + if ($Languages -eq '*') + { + $WsusConfiguration.AllUpdateLanguagesEnabled = $true + } + else + { + $WsusConfiguration.AllUpdateLanguagesEnabled = $false + $WsusConfiguration.SetEnabledUpdateLanguages($Languages) + } + } + } + else + { + Write-Verbose -Message $script:localizedData.ConfiguringHostMUStore + $WsusConfiguration.HostBinariesOnMicrosoftUpdate = $true + } } - else + + # If this is not a replica server + if (-not $UpstreamServerReplica) { - $WsusConfiguration.AllUpdateLanguagesEnabled = $false - $WsusConfiguration.SetEnabledUpdateLanguages($Languages) + # Configure Advanced Automatic Approvals + if ($PSBoundParameters.ContainsKey('AutoApproveWsusInfrastructureUpdates')) + { + Write-Verbose -Message $script:localizedData.ConfiguringAutoApproveWsusInfrastructureUpdates + $WsusConfiguration.AutoApproveWsusInfrastructureUpdates = $AutoApproveWsusInfrastructureUpdates + } + if ($PSBoundParameters.ContainsKey('AutoRefreshUpdateApprovals')) + { + Write-Verbose -Message $script:localizedData.ConfiguringAutoRefreshUpdateApprovals + $WsusConfiguration.AutoRefreshUpdateApprovals = $AutoRefreshUpdateApprovals + if ($AutoRefreshUpdateApprovals) + { + if ($PSBoundParameters.ContainsKey('AutoRefreshUpdateApprovalsDeclineExpired')) + { + Write-Verbose -Message $script:localizedData.ConfiguringAutoRefreshUpdateApprovalsDeclineExpired + $WsusConfiguration.AutoRefreshUpdateApprovalsDeclineExpired = $AutoRefreshUpdateApprovalsDeclineExpired + } + } + } } #ClientTargetingMode @@ -500,214 +1004,422 @@ function Set-TargetResource $WsusConfiguration.TargetingMode = $ClientTargetingMode } - # Save configuration before initial sync - SaveWsusConfiguration - - # Post Install - if ($PostInstall) + # Configure Email notifications + if ($PSBoundParameters.ContainsKey('SyncNotificationRecipients')) { - Write-Verbose -Message $script:localizedData.RemovingDefaultInit - # remove default products & classification - foreach ($Product in ($WsusServer.GetSubscription().GetUpdateCategories().Title)) + if ($SyncNotificationRecipients) + { + Write-Verbose -Message $script:localizedData.ConfiguringSyncNotificationRecipients + $WsusEmailNotificationConfiguration.SendSyncNotification = $true + $WsusEmailNotificationConfiguration.SyncNotificationRecipients.Clear() + foreach ($syncNotificationRecipient in $SyncNotificationRecipients) + { + $WsusEmailNotificationConfiguration.SyncNotificationRecipients.Add($syncNotificationRecipient) + } + } + else { - Get-WsusProduct | Where-Object -FilterScript { $_.Product.Title -eq $Product } | ` - Set-WsusProduct -Disable + Write-Verbose -Message $script:localizedData.ConfiguringNoSyncNotificationRecipients + $WsusEmailNotificationConfiguration.SendSyncNotification = $false + } } - foreach ($Classification in ` - ($WsusServer.GetSubscription().GetUpdateClassifications().ID.Guid)) - { - Get-WsusClassification | Where-Object -FilterScript { $_.Classification.ID -eq $Classification } | ` - Set-WsusClassification -Disable - } - - if ($Synchronize) - { - Write-Verbose -Message $script:localizedData.RunningInitSync - $WsusServer.GetSubscription().StartSynchronizationForCategoryOnly() - while ($WsusServer.GetSubscription().GetSynchronizationStatus() -eq 'Running') + if ($PSBoundParameters.ContainsKey('StatusNotificationRecipients')) { - Start-Sleep -Seconds 1 - } - - if ($WsusServer.GetSubscription().GetSynchronizationHistory()[0].Result -eq 'Succeeded') - { - Write-Verbose -Message $script:localizedData.InitSyncSuccess - $WsusConfiguration.OobeInitialized = $true - SaveWsusConfiguration - } - else - { - Write-Verbose -Message $script:localizedData.InitSyncFailure - } - } - else - { - Write-Verbose -Message $script:localizedData.RunningInitOfflineSync + if ($StatusNotificationRecipients) + { + Write-Verbose -Message $script:localizedData.ConfiguringStatusNotificationRecipients + $WsusEmailNotificationConfiguration.SendStatusNotification = $true + $WsusEmailNotificationConfiguration.StatusNotificationRecipients.Clear() + foreach ($statusNotificationRecipient in $StatusNotificationRecipients) + { + $WsusEmailNotificationConfiguration.StatusNotificationRecipients.Add($statusNotificationRecipient) + } + if ($PSBoundParameters.ContainsKey('StatusNotificationFrequency')) + { + Write-Verbose -Message $script:localizedData.ConfiguringStatusNotificationFrequency + $WsusEmailNotificationConfiguration.StatusNotificationFrequency = $StatusNotificationFrequency + } + if ($PSBoundParameters.ContainsKey('StatusNotificationTimeOfDay')) + { + Write-Verbose -Message $script:localizedData.ConfiguringStatusNotificationTimeOfDay - $TempFile = [IO.Path]::GetTempFileName() + $currentDateTime = Get-Date - $CABPath = Join-Path -Path $PSScriptRoot -ChildPath '\WSUS.cab' + # When Daylight Savings Time is in effect, StatusNotificationTimeOfDay needs to be set as UTC with the DST offset deducted + # Must remove the DST offset before applying to set the actual time - see https://learn.microsoft.com/en-us/previous-versions/windows/desktop/aa351886(v=vs.85) + if ($currentDateTime.IsDaylightSavingTime()) + { + $currentTimeZone = Get-TimeZone - $Arguments = 'import ' - $Arguments += "`"$CABPath`" $TempFile" + # Convert StatusNotificationTimeOfDay from a String to a DateTimeOffset value defined in UTC + $StatusNotificationTimeOfDayDateTimeOffset = [datetimeoffset]::Parse("$($StatusNotificationTimeOfDay)Z") - Write-Verbose -Message ($script:localizedData.WsusUtilArgs -f $Arguments) + # Subtract the currently active DST offset from the supplied DateTimeOffset to get UTC TimeOfDay as TimeSpan + $StatusNotificationTimeOfDay = $StatusNotificationTimeOfDayDateTimeOffset - ([datetimeoffset]$currentDateTime).Offset + $currentTimeZone.BaseUtcOffset | Select-Object -ExpandProperty TimeOfDay + } - if ($SetupCredential) - { - $Process = Start-Win32Process -Path $Path -Arguments $Arguments -Credential $SetupCredential - Write-Verbose -Message [string]$Process - Wait-Win32ProcessEnd -Path $Path -Arguments $Arguments - } - else - { - $Process = Start-Win32Process -Path $Path -Arguments $Arguments - Write-Verbose -Message [string]$Process - Wait-Win32ProcessEnd -Path $Path -Arguments $Arguments + $WsusEmailNotificationConfiguration.StatusNotificationTimeOfDay = $StatusNotificationTimeOfDay + } + } + else + { + Write-Verbose -Message $script:localizedData.ConfiguringNoStatusNotificationRecipients + $WsusEmailNotificationConfiguration.SendStatusNotification = $false + } } - $WsusConfiguration.OobeInitialized = $true - SaveWsusConfiguration - } -} -# Configure WSUS subscription -if ($WsusConfiguration.OobeInitialized) -{ - $wsusSubscription = $WsusServer.GetSubscription() - - # Products - Write-Verbose -Message $script:localizedData.ConfiguringProducts - $productCollection = New-Object Microsoft.UpdateServices.Administration.UpdateCategoryCollection - $allWsusProducts = $WsusServer.GetUpdateCategories() - - switch ($Products) - { - # All Products - '*' { - Write-Verbose -Message $script:localizedData.ConfiguringAllProducts - foreach ($prdct in $AllWsusProducts) + if ($PSBoundParameters.ContainsKey('EmailLanguage')) + { + Write-Verbose -Message $script:localizedData.ConfiguringEmailLanguage + if ($WsusEmailNotificationConfiguration.SupportedEmailLanguages -contains $EmailLanguage) { - $null = $productCollection.Add($WsusServer.GetUpdateCategory($prdct.Id)) + $WsusEmailNotificationConfiguration.EmailLanguage = $EmailLanguage } - continue + else + { + New-InvalidOperationException -Message ($script:localizedData.UnsupportedEmailLanguage -f $EmailLanguage) + } + } + + if ($PSBoundParameters.ContainsKey('SmtpHostName')) + { + Write-Verbose -Message $script:localizedData.ConfiguringSmtpHostName + $WsusEmailNotificationConfiguration.SmtpHostName = $SmtpHostName + $WsusEmailNotificationConfiguration.SmtpPort = $SmtpPort + } + if ($PSBoundParameters.ContainsKey('SenderDisplayName')) + { + Write-Verbose -Message $script:localizedData.ConfiguringSenderDisplayName + $WsusEmailNotificationConfiguration.SenderDisplayName = $SenderDisplayName } - # if Products property contains wildcard like "Windows*" - {[System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($_)} { - $wildcardPrdct = $_ - Write-Verbose -Message $($script:localizedData.ConfiguringWildcardProducts -f $wildcardPrdct) - if ($wsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -like $wildcardPrdct }) + if ($PSBoundParameters.ContainsKey('SenderEmailAddress')) + { + Write-Verbose -Message $script:localizedData.ConfiguringSenderEmailAddress + # If SenderEmailAddress is an empty string (''), need to set to $null instead + if ($SenderEmailAddress) { - foreach ($prdct in $wsusProduct) - { - $null = $productCollection.Add($WsusServer.GetUpdateCategory($prdct.Id)) - } + $WsusEmailNotificationConfiguration.SenderEmailAddress = $SenderEmailAddress } else { - Write-Verbose -Message $script:localizedData.NoWildcardProductFound + $WsusEmailNotificationConfiguration.SenderEmailAddress = $null } - continue } - <# - We can try to add GUID support for product with : - - $StringGuid ="077e4982-4dd1-4d1f-ba18-d36e419971c1" - $ObjectGuid = [System.Guid]::New($StringGuid) - $IsEmptyGUID = $ObjectGuid -eq [System.Guid]::empty - - Maybe with function - #> - - default { - Write-Verbose -Message $($script:localizedData.ConfiguringNameProduct -f $_) - $prdct = $_ - if ($WsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -eq $prdct }) + if ($SmtpHostName) + { + if ($PSBoundParameters.ContainsKey('EmailServerCredential')) { - foreach ($pdt in $WsusProduct) + if ($EmailServerCredential) + { + Write-Verbose -Message $script:localizedData.ConfiguringEmailServerCredential + $WsusEmailNotificationConfiguration.SmtpServerRequiresAuthentication = $true + $WsusEmailNotificationConfiguration.SmtpUserName = $EmailServerCredential.GetNetworkCredential().UserName + $WsusEmailNotificationConfiguration.SetSmtpUserPassword($EmailServerCredential.GetNetworkCredential().Password) + } + else { - $null = $productCollection.Add($WsusServer.GetUpdateCategory($pdt.Id)) + Write-Verbose -Message $script:localizedData.RemovingEmailServerCredential + $WsusEmailNotificationConfiguration.SmtpServerRequiresAuthentication = $false } } + } + + # Save WSUS email configuration + $WsusEmailNotificationConfiguration.Save() + + # Configure IIS dynamic compression + if ($PSBoundParameters.ContainsKey('IIsDynamicCompression')) + { + Write-Verbose -Message $script:localizedData.ConfiguringIIsDynamicCompression + if ($IIsDynamicCompression) + { + & $env:SystemRoot\System32\cscript.exe "$env:ProgramFiles\Update Services\Setup\DynamicCompression.vbs" /enable "$env:ProgramFiles\Update Services\WebServices\suscomp.dll" | Out-Null + } else { - Write-Verbose -Message $script:localizedData.NoNameProductFound + & $env:SystemRoot\System32\cscript.exe "$env:ProgramFiles\Update Services\Setup\DynamicCompression.vbs" /disable | Out-Null } } - } - $wsusSubscription.SetUpdateCategories($ProductCollection) + # Configure BITS download priority foreground + if ($PSBoundParameters.ContainsKey('BitsDownloadPriorityForeground')) + { + Write-Verbose -Message $script:localizedData.ConfiguringBitsDownloadPriorityForeground + $WsusConfiguration.BitsDownloadPriorityForeground = $BitsDownloadPriorityForeground + } - # Classifications - Write-Verbose -Message $script:localizedData.ConfiguringClassifications - $ClassificationCollection = New-Object Microsoft.UpdateServices.Administration.UpdateClassificationCollection - $AllWsusClassifications = $WsusServer.GetUpdateClassifications() - if ($Classifications -eq '*') - { - foreach ($Classification in $AllWsusClassifications) + # Configure local publishing + if ($PSBoundParameters.ContainsKey('LocalPublishingMaxCabSize')) { - $null = $ClassificationCollection.Add($WsusServer.GetUpdateClassification($Classification.Id)) + Write-Verbose -Message $script:localizedData.ConfiguringLocalPublishing + $WsusConfiguration.LocalPublishingMaxCabSize = $LocalPublishingMaxCabSize } - } - else - { - foreach ($Classification in $Classifications) + + # Configure max simultaneous file downloads + if ($PSBoundParameters.ContainsKey('MaxSimultaneousFileDownloads')) + { + Write-Verbose -Message $script:localizedData.ConfiguringMaxSimultaneousFileDownloads + $WsusConfiguration.MaxSimultaneousFileDownloads = $MaxSimultaneousFileDownloads + } + + # Save configuration - avoid 'Operation is not valid due to the current state of the object' when DoDetailedRollup is being set + Save-WsusConfiguration + + # If this is not a replica server + if (-not $UpstreamServerReplica) { - if ($WsusClassification = $AllWsusClassifications | Where-Object -FilterScript { $_.ID.Guid -eq $Classification }) + # Configure Reporting rollup + if ($PSBoundParameters.ContainsKey('DoDetailedRollup')) { - $null = $ClassificationCollection.Add( - $WsusServer.GetUpdateClassification($WsusClassification.Id) - ) + Write-Verbose -Message $script:localizedData.ConfiguringReportingRollup + $WsusConfiguration.DoDetailedRollup = $DoDetailedRollup } - else + } + + # Save configuration before initial sync + Save-WsusConfiguration + + # If the initial configuration wizard flag is not yet set, perform initial sync + if (-not $WsusConfiguration.OobeInitialized) + { + # If this is not a replica server + if (-not $UpstreamServerReplica) { - Write-Verbose -Message ($script:localizedData.ClassificationNotFound -f $Classification) + if ($PSBoundParameters.ContainsKey('Products')) + { + Write-Verbose -Message $script:localizedData.RemovingDefaultProductsInit + # remove default products + foreach ($Product in ($WsusServer.GetSubscription().GetUpdateCategories().Title)) + { + Get-WsusProduct | Where-Object -FilterScript { $_.Product.Title -eq $Product } | + Set-WsusProduct -Disable + } + } + + if ($PSBoundParameters.ContainsKey('Classifications')) + { + Write-Verbose -Message $script:localizedData.RemovingDefaultClassificationsInit + # remove default classifications + foreach ($Classification in ` + ($WsusServer.GetSubscription().GetUpdateClassifications().ID.Guid)) + { + Get-WsusClassification | Where-Object -FilterScript { $_.Classification.ID -eq $Classification } | + Set-WsusClassification -Disable + } + } } - } - } - $WsusSubscription.SetUpdateClassifications($ClassificationCollection) + if ($Synchronize) + { + Write-Verbose -Message $script:localizedData.RunningInitSync + $WsusServer.GetSubscription().StartSynchronizationForCategoryOnly() + while ($WsusServer.GetSubscription().GetSynchronizationStatus() -eq 'Running') + { + Start-Sleep -Seconds 1 + } - #Synchronization Schedule - Write-Verbose -Message $script:localizedData.ConfiguringSyncSchedule - $WsusSubscription.SynchronizeAutomatically = $SynchronizeAutomatically - if ($PSBoundParameters.ContainsKey('SynchronizeAutomaticallyTimeOfDay')) - { - $WsusSubscription.SynchronizeAutomaticallyTimeOfDay = $SynchronizeAutomaticallyTimeOfDay - } + if ($WsusServer.GetSubscription().GetSynchronizationHistory()[0].Result -eq 'Succeeded') + { + Write-Verbose -Message $script:localizedData.InitSyncSuccess + $WsusConfiguration.OobeInitialized = $true + Save-WsusConfiguration + } + else + { + Write-Verbose -Message $script:localizedData.InitSyncFailure + } + } + else + { + Write-Verbose -Message $script:localizedData.RunningInitOfflineSync - $WsusSubscription.NumberOfSynchronizationsPerDay = $SynchronizationsPerDay - $WsusSubscription.Save() + $TempFile = [IO.Path]::GetTempFileName() - if ($Synchronize) - { - Write-Verbose -Message $script:localizedData.SynchronizingWsus + $CABPath = Join-Path -Path $PSScriptRoot -ChildPath '\WSUS.cab' - $WsusServer.GetSubscription().StartSynchronization() - while ($WsusServer.GetSubscription().GetSynchronizationStatus() -eq 'Running') - { - Start-Sleep -Seconds 1 - } + $Arguments = 'import ' + $Arguments += "`"$CABPath`" $TempFile" - if ($WsusServer.GetSubscription().GetSynchronizationHistory()[0].Result -eq 'Succeeded') - { - Write-Verbose -Message $script:localizedData.InitSyncSuccess + Write-Verbose -Message ($script:localizedData.WsusUtilArgs -f $Arguments) + + if ($PSBoundParameters.ContainsKey('SetupCredential')) + { + $Process = Start-Win32Process -Path $Path -Arguments $Arguments -Credential $SetupCredential + Write-Verbose -Message $Process + Wait-Win32ProcessEnd -Path $Path -Arguments $Arguments + } + else + { + $Process = Start-Win32Process -Path $Path -Arguments $Arguments + Write-Verbose -Message $Process + Wait-Win32ProcessEnd -Path $Path -Arguments $Arguments + } + + $WsusConfiguration.OobeInitialized = $true + Save-WsusConfiguration + } } - else + + # If the initial configuration wizard flag is already set, configure WSUS subscription + if ($WsusConfiguration.OobeInitialized) { - Write-Verbose -Message $script:localizedData.InitSyncFailure + $wsusSubscription = $WsusServer.GetSubscription() + + # If this is not a replica server + if (-not $UpstreamServerReplica) + { + # Products + if ($PSBoundParameters.ContainsKey('Products')) + { + Write-Verbose -Message $script:localizedData.ConfiguringProducts + $productCollection = New-Object Microsoft.UpdateServices.Administration.UpdateCategoryCollection + $allWsusProducts = $WsusServer.GetUpdateCategories() + + switch ($Products) + { + # All Products + '*' { + Write-Verbose -Message $script:localizedData.ConfiguringAllProducts + foreach ($prdct in $AllWsusProducts) + { + $null = $productCollection.Add($WsusServer.GetUpdateCategory($prdct.Id)) + } + continue + } + # if Products property contains wildcard like "Windows*" + {[System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($_)} { + $wildcardPrdct = $_ + Write-Verbose -Message $($script:localizedData.ConfiguringWildcardProducts -f $wildcardPrdct) + if ($wsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -like $wildcardPrdct }) + { + foreach ($prdct in $wsusProduct) + { + $null = $productCollection.Add($WsusServer.GetUpdateCategory($prdct.Id)) + } + } + else + { + Write-Verbose -Message $script:localizedData.NoWildcardProductFound + } + continue + } + + <# + We can try to add GUID support for product with : + + $StringGuid ="077e4982-4dd1-4d1f-ba18-d36e419971c1" + $ObjectGuid = [System.Guid]::New($StringGuid) + $IsEmptyGUID = $ObjectGuid -eq [System.Guid]::empty + + Maybe with function + #> + + default { + Write-Verbose -Message $($script:localizedData.ConfiguringNameProduct -f $_) + $prdct = $_ + if ($WsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -eq $prdct }) + { + foreach ($pdt in $WsusProduct) + { + $null = $productCollection.Add($WsusServer.GetUpdateCategory($pdt.Id)) + } + } + else + { + Write-Verbose -Message $script:localizedData.NoNameProductFound + } + } + } + + $wsusSubscription.SetUpdateCategories($ProductCollection) + } + + # Classifications + if ($PSBoundParameters.ContainsKey('Classifications')) + { + Write-Verbose -Message $script:localizedData.ConfiguringClassifications + $ClassificationCollection = New-Object Microsoft.UpdateServices.Administration.UpdateClassificationCollection + $AllWsusClassifications = $WsusServer.GetUpdateClassifications() + if ($Classifications -eq '*') + { + foreach ($Classification in $AllWsusClassifications) + { + $null = $ClassificationCollection.Add($WsusServer.GetUpdateClassification($Classification.Id)) + } + } + else + { + foreach ($Classification in $Classifications) + { + if ($WsusClassification = $AllWsusClassifications | Where-Object -FilterScript { $_.ID.Guid -eq $Classification }) + { + $null = $ClassificationCollection.Add( + $WsusServer.GetUpdateClassification($WsusClassification.Id) + ) + } + else + { + Write-Verbose -Message ($script:localizedData.ClassificationNotFound -f $Classification) + } + } + } + + $WsusSubscription.SetUpdateClassifications($ClassificationCollection) + } + } + + #Synchronization Schedule + if ($PSBoundParameters.ContainsKey('SynchronizeAutomatically')) + { + Write-Verbose -Message $script:localizedData.ConfiguringSyncSchedule + $WsusSubscription.SynchronizeAutomatically = $SynchronizeAutomatically + if ($SynchronizeAutomatically) + { + if ($PSBoundParameters.ContainsKey('SynchronizeAutomaticallyTimeOfDay')) + { + Write-Verbose -Message $script:localizedData.ConfiguringSyncTimeOfDay + $WsusSubscription.SynchronizeAutomaticallyTimeOfDay = $SynchronizeAutomaticallyTimeOfDay + } + if ($PSBoundParameters.ContainsKey('SynchronizationsPerDay')) + { + Write-Verbose -Message $script:localizedData.ConfiguringSyncPerDay + $WsusSubscription.NumberOfSynchronizationsPerDay = $SynchronizationsPerDay + } + } + } + + $WsusSubscription.Save() + + if ($Synchronize) + { + Write-Verbose -Message $script:localizedData.SynchronizingWsus + + $WsusServer.GetSubscription().StartSynchronization() + while ($WsusServer.GetSubscription().GetSynchronizationStatus() -eq 'Running') + { + Start-Sleep -Seconds 1 + } + + if ($WsusServer.GetSubscription().GetSynchronizationHistory()[0].Result -eq 'Succeeded') + { + Write-Verbose -Message $script:localizedData.InitSyncSuccess + } + else + { + Write-Verbose -Message $script:localizedData.InitSyncFailure + } + } } } -} -} -if (-not (Test-TargetResource @PSBoundParameters)) -{ - $errorMessage = $script:localizedData.TestFailedAfterSet - New-InvalidResultException -Message $errorMessage -} + if (-not (Test-TargetResource @PSBoundParameters)) + { + $errorMessage = $script:localizedData.TestFailedAfterSet + New-InvalidResultException -Message $errorMessage + } } <# @@ -726,7 +1438,8 @@ if (-not (Test-TargetResource @PSBoundParameters)) Optionally specify a SQL instance to store WSUS data .PARAMETER ContentDir - Location to store WSUS content files + Location to store WSUS content files. + Set as empty string ('') to download from Microsoft Update. .PARAMETER UpdateImprovementProgram Provide feedback to Microsoft to help improve WSUS @@ -755,6 +1468,18 @@ if (-not (Test-TargetResource @PSBoundParameters)) .PARAMETER ProxyServerBasicAuthentication Use basic auth for proxy + .PARAMETER ProxyServerBasicAuthentication + Use basic auth for proxy + + .PARAMETER DownloadUpdateBinariesAsNeeded + Updates are downloaded only when they are approved + + .PARAMETER DownloadExpressPackages + Express installation packages should be downloaded + + .PARAMETER GetContentFromMU + Update binaries are downloaded from Microsoft Update instead of from the upstream server + .PARAMETER Languages Specify list of languages for content, or '*' for all @@ -768,7 +1493,7 @@ if (-not (Test-TargetResource @PSBoundParameters)) Automatically synchronize the WSUS instance .PARAMETER SynchronizeAutomaticallyTimeOfDay - Time of day to schedule an automatic synchronization + Time of day to schedule an automatic synchronization (as UTC) .PARAMETER SynchronizationsPerDay Number of automatic synchronizations per day @@ -776,10 +1501,70 @@ if (-not (Test-TargetResource @PSBoundParameters)) .PARAMETER Synchronize Run a synchronization immediately when running Set + .PARAMETER AutoApproveWsusInfrastructureUpdates + WSUS infrastructure updates are approved automatically + + .PARAMETER AutoRefreshUpdateApprovals + The latest revision of an update should be approved automatically + + .PARAMETER AutoRefreshUpdateApprovalsDeclineExpired + An update should be automatically declined when it is revised to be expired and + AutoRefreshUpdateApprovals is enabled + .PARAMETER ClientTargetingMode - An enumerated value that describes if how the Target Groups are populated. + An enumerated value that describes how the Target Groups are populated. Accepts 'Client'(default) or 'Server'. + .PARAMETER DoDetailedRollup + The downstream server should roll up detailed computer and update status information + + .PARAMETER SyncNotificationRecipients + E-mail addresses of those to whom notification of new updates should be sent, omit for no notifications + + .PARAMETER StatusNotificationFrequency + The frequency with which e-mail notifications should be sent + Accepts 'Daily'(default) or 'Weekly' + + .PARAMETER StatusNotificationTimeOfDay + The time of the day e-mail notifications should be sent (as UTC) + The value must be a string representation of a TimeSpan value + The valid range is 00:00:00 to 23:59:59 inclusive + + .PARAMETER StatusNotificationRecipients + E-mail addresses of those to whom update status notification should be sent, omit for no notifications + + .PARAMETER EmailLanguage + E-mail language setting + + .PARAMETER SmtpHostName + The host name of the SMTP server + + .PARAMETER SmtpPort + The port number of the SMTP server + + .PARAMETER SenderDisplayName + The display name of the e-mail sender + + .PARAMETER SenderEmailAddress + The e-mail address of the sender + + .PARAMETER EmailServerCredential + The e-mail server credential, omit for anonymous. + + .PARAMETER IIsDynamicCompression + Use Xpress Encoding to compress update metadata. + Results in significant bandwidth savings, at the expense of some CPU overhead. + + .PARAMETER BitsDownloadPriorityForeground + Use foreground priority for BITS downloads to handle issues with proxy servers that do not correctly handle + HTTP 1.1 range request. + + .PARAMETER LocalPublishingMaxCabSize + The maximum .cab file size (in megabytes) that Local Publishing will create + + .PARAMETER MaxSimultaneousFileDownloads + The maximum number of concurrent update downloads + #> function Test-TargetResource { @@ -818,7 +1603,7 @@ function Test-TargetResource [Parameter()] [System.Boolean] - $UpstreamServerSSL, + $UpstreamServerSSL = $false, [Parameter()] [System.Boolean] @@ -838,29 +1623,45 @@ function Test-TargetResource [Parameter()] [System.Boolean] - $ProxyServerBasicAuthentication, + $ProxyServerBasicAuthentication = $false, + + [Parameter()] + [System.Boolean] + $DownloadUpdateBinariesAsNeeded, + + [Parameter()] + [System.Boolean] + $DownloadExpressPackages, + + [Parameter()] + [System.Boolean] + $GetContentFromMU, [Parameter()] [System.String[]] - $Languages = @('*'), + $Languages, [Parameter()] [System.String[]] - $Products = @('Windows', 'Office'), + $Products, [Parameter()] [System.String[]] - $Classifications = @('E6CF1350-C01B-414D-A61F-263D14D133B4', 'E0789628-CE08-4437-BE74-2495B842F43B', '0FA1201D-4330-4FA8-8AE9-B877473B6441'), + $Classifications, [Parameter()] [System.Boolean] $SynchronizeAutomatically, [Parameter()] + [ValidateScript({ + ([ValidateRange(0, 86399)]$valueInSeconds = [TimeSpan]::Parse($_).TotalSeconds); $? + })] [System.String] $SynchronizeAutomaticallyTimeOfDay, [Parameter()] + [ValidateRange(1, 24)] [System.UInt16] $SynchronizationsPerDay = 1, @@ -868,233 +1669,579 @@ function Test-TargetResource [System.Boolean] $Synchronize, + [Parameter()] + [System.Boolean] + $AutoApproveWsusInfrastructureUpdates, + + [Parameter()] + [System.Boolean] + $AutoRefreshUpdateApprovals, + + [Parameter()] + [System.Boolean] + $AutoRefreshUpdateApprovalsDeclineExpired, + [Parameter()] [ValidateSet('Client', 'Server')] [System.String] - $ClientTargetingMode + $ClientTargetingMode, + + [Parameter()] + [System.Boolean] + $DoDetailedRollup, + + [Parameter()] + [System.String[]] + $SyncNotificationRecipients, + + [Parameter()] + [ValidateSet('Daily', 'Weekly')] + [System.String] + $StatusNotificationFrequency, + + [Parameter()] + [ValidateScript({ + ([ValidateRange(0, 86399)]$valueInSeconds = [TimeSpan]::Parse($_).TotalSeconds); $? + })] + [System.String] + $StatusNotificationTimeOfDay, + + [Parameter()] + [System.String[]] + $StatusNotificationRecipients, + + [Parameter()] + [System.String] + $EmailLanguage, + + [Parameter()] + [System.String] + $SmtpHostName, + + [Parameter()] + [System.UInt16] + $SmtpPort = 25, + + [Parameter()] + [System.String] + $SenderDisplayName, + + [Parameter()] + [System.String] + $SenderEmailAddress, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EmailServerCredential, + + [Parameter()] + [System.Boolean] + $IIsDynamicCompression, + + [Parameter()] + [System.Boolean] + $BitsDownloadPriorityForeground, + + [Parameter()] + [System.UInt32] + $LocalPublishingMaxCabSize, + + [Parameter()] + [System.UInt32] + $MaxSimultaneousFileDownloads ) + Assert-Module -ModuleName UpdateServices + $Wsus = Get-TargetResource -Ensure $Ensure - # Test Ensure + # Test Ensure - if incorrect, return immediately without testing if ($Wsus.Ensure -ne $Ensure) { Write-Verbose -Message $script:localizedData.EnsureTestFailed return $false } - # Test Update Improvement Program - if ($Wsus.UpdateImprovementProgram -ne $UpdateImprovementProgram) - { - Write-Verbose -Message $script:localizedData.ImproveProgramTestFailed - return $false - } - - # Test Upstream Server - if ($Wsus.UpstreamServerName -ne $UpstreamServerName) - { - Write-Verbose -Message $script:localizedData.UpstreamNameTestFailed - return $false - } + # Flag to signal whether settings are correct + $testTargetResourceReturnValue = $true - if ($PSBoundParameters.ContainsKey('UpstreamServerName')) + if ($Ensure -eq 'Present') { - if ($Wsus.UpstreamServerPort -ne $UpstreamServerPort) + # If this is not a replica server + if (-not $UpstreamServerReplica) { - Write-Verbose -Message $script:localizedData.UpstreamPortTestFailed - return $false + # Test Update Improvement Program + if ($PSBoundParameters.ContainsKey('UpdateImprovementProgram')) + { + if ($Wsus.UpdateImprovementProgram -ne $UpdateImprovementProgram) + { + Write-Verbose -Message $script:localizedData.ImproveProgramTestFailed + $testTargetResourceReturnValue = $false + } + } } - if ($Wsus.UpstreamServerSSL -ne $UpstreamServerSSL) + # Test Upstream Server + if ($PSBoundParameters.ContainsKey('UpstreamServerName')) { - Write-Verbose -Message $script:localizedData.UpstreamSSLTestFailed - return $false + if ($Wsus.UpstreamServerName -ne $UpstreamServerName) + { + Write-Verbose -Message $script:localizedData.UpstreamNameTestFailed + $testTargetResourceReturnValue = $false + } + + if ($UpstreamServerName) + { + if ($Wsus.UpstreamServerPort -ne $UpstreamServerPort) + { + Write-Verbose -Message $script:localizedData.UpstreamPortTestFailed + $testTargetResourceReturnValue = $false + } + if ($Wsus.UpstreamServerSSL -ne $UpstreamServerSSL) + { + Write-Verbose -Message $script:localizedData.UpstreamSSLTestFailed + $testTargetResourceReturnValue = $false + } + } } - if ($Wsus.UpstreamServerReplica -ne $UpstreamServerReplica) + # Test Upstream Server Replica separately as IsReplicaServer=$true prevents other settings even when SyncFromMicrosoftUpdate=$true + if ($PSBoundParameters.ContainsKey('UpstreamServerReplica')) { - Write-Verbose -Message $script:localizedData.UpstreamReplicaTestFailed - return $false + if ($UpstreamServerName) + { + if ($Wsus.UpstreamServerReplica -ne $UpstreamServerReplica) + { + Write-Verbose -Message $script:localizedData.UpstreamReplicaTestFailed + $testTargetResourceReturnValue = $false + } + } + else + { + if (-not $UpstreamServerReplica -and $Wsus.UpstreamServerReplica -eq $true) # If no upstream server is configured, only fail the test if this is true + { + Write-Verbose -Message $script:localizedData.NoUpstreamServerReplicaTestFailed + $testTargetResourceReturnValue = $false + } + } + } - } - # Test Proxy Server - if ($Wsus.ProxyServerName -ne $ProxyServerName) - { - Write-Verbose -Message $script:localizedData.ProxyNameTestFailed - return $false - } + # Test Proxy Server + if ($PSBoundParameters.ContainsKey('ProxyServerName')) + { + if ($Wsus.ProxyServerName -ne $ProxyServerName) + { + Write-Verbose -Message $script:localizedData.ProxyNameTestFailed + $testTargetResourceReturnValue = $false + } - if ($PSBoundParameters.ContainsKey('ProxyServerName')) - { - if ($Wsus.ProxyServerPort -ne $ProxyServerPort) + if ($ProxyServerName) + { + if ($Wsus.ProxyServerPort -ne $ProxyServerPort) + { + Write-Verbose -Message $script:localizedData.ProxyPortTestFailed + $testTargetResourceReturnValue = $false + } + if ($PSBoundParameters.ContainsKey('ProxyServerCredential')) + { + # Ensure that ProxyServerCredential is returned as string - if empty, otherwise returns $null + if ($Wsus.ProxyServerCredentialUserName -ne [String]$ProxyServerCredential.UserName) + { + Write-Verbose -Message $script:localizedData.ProxyCredTestFailed + $testTargetResourceReturnValue = $false + } + + if ($ProxyServerCredential) + { + if ($PSBoundParameters.ContainsKey('ProxyServerBasicAuthentication')) + { + if ($Wsus.ProxyServerBasicAuthentication -ne $ProxyServerBasicAuthentication) + { + Write-Verbose -Message $script:localizedData.ProxyBasicAuthTestFailed + $testTargetResourceReturnValue = $false + } + } + } + } + } + } + + # Test Update Files + if ($PSBoundParameters.ContainsKey('ContentDir')) { - Write-Verbose -Message $script:localizedData.ProxyPortTestFailed - return $false + if ((Join-Path $Wsus.ContentDir '') -ne (Join-Path $ContentDir '')) + { + Write-Verbose -Message $script:localizedData.ContentDirTestFailed + $testTargetResourceReturnValue = $false + } + + if ($ContentDir) + { + if ($PSBoundParameters.ContainsKey('DownloadUpdateBinariesAsNeeded')) + { + if ($Wsus.DownloadUpdateBinariesAsNeeded -ne $DownloadUpdateBinariesAsNeeded) + { + Write-Verbose -Message $script:localizedData.UpdateFilesDownloadUpdateBinariesTestFailed + $testTargetResourceReturnValue = $false + } + } + if ($PSBoundParameters.ContainsKey('DownloadExpressPackages')) + { + if ($Wsus.DownloadExpressPackages -ne $DownloadExpressPackages) + { + Write-Verbose -Message $script:localizedData.UpdateFilesDownloadExpressPackagesTestFailed + $testTargetResourceReturnValue = $false + } + } + if ($UpstreamServerName -eq '') + { + if ($PSBoundParameters.ContainsKey('GetContentFromMU')) + { + if ($Wsus.GetContentFromMU -ne $GetContentFromMU) + { + Write-Verbose -Message $script:localizedData.UpdateFilesGetContentFromMUTestFailed + $testTargetResourceReturnValue = $false + } + } + } + + # Test Languages + if ($PSBoundParameters.ContainsKey('Languages')) + { + if ($Wsus.Languages.count -le 1 -and $Languages.count -le 1 -and $Languages -ne '*') + { + if ($Wsus.Languages -notmatch $Languages) + { + Write-Verbose -Message $script:localizedData.LanguageAsStrTestFailed + $testTargetResourceReturnValue = $false + } + } + else + { + if ($null -ne (Compare-Object -ReferenceObject ($Wsus.Languages | Sort-Object -Unique) ` + -DifferenceObject ($Languages | Sort-Object -Unique) -SyncWindow 0)) + { + Write-Verbose -Message $script:localizedData.LanguageSetTestFailed + $testTargetResourceReturnValue = $false + } + } + } + } } - if ($PSBoundParameters.ContainsKey('ProxyServerCredential')) + # If this is not a replica server + if (-not $UpstreamServerReplica) { - if ( - ($null -eq $Wsus.ProxyServerCredentialUserName) -or - ($Wsus.ProxyServerCredentialUserName -ne $ProxyServerCredential.UserName) - ) + # Test Products + if ($PSBoundParameters.ContainsKey('Products')) + { + try + { + $wsusServer = Get-WsusServer -ErrorAction Stop + } + catch + { + Write-Verbose -Message $script:localizedData.TestGetWsusServer + $testTargetResourceReturnValue = $false + } + $allWsusProducts = $wsusServer.GetUpdateCategories() + [System.Collections.ArrayList]$productCollection = @() + + switch ($Products) + { + # All Products + '*' { + Write-Verbose -Message $script:localizedData.GetAllProductForTest + $null = $productCollection.Add('*') + continue + } + # if Products property contains wild card like "Windows*" + {[System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($_)} { + $wildcardPrdct = $_ + Write-Verbose -Message $($script:localizedData.GetWildCardProductForTest -f $wildcardPrdct) + if ($wsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -like $wildcardPrdct }) + { + foreach ($pdt in $wsusProduct) + { + $null = $productCollection.Add($pdt.Title) + } + } + else + { + Write-Verbose -Message $script:localizedData.NoWildcardProductFound + } + continue + } + + <# + We can try to add GUID support for product with : + + $StringGuid ="077e4982-4dd1-4d1f-ba18-d36e419971c1" + $ObjectGuid = [System.Guid]::New($StringGuid) + $IsEmptyGUID = $ObjectGuid -eq [System.Guid]::empty + + Maybe with function + #> + + default { + $prdct = $_ + Write-Verbose -Message $($script:localizedData.GetNameProductForTest -f $prdct) + if ($wsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -eq $prdct }) + { + foreach ($pdt in $wsusProduct) + { + $null = $ProductCollection.Add($pdt.Title) + } + } + else + { + Write-Verbose -Message $script:localizedData.NoNameProductFound + } + } + } + + + if ($null -ne (Compare-Object -ReferenceObject ($Wsus.Products | Sort-Object -Unique) ` + -DifferenceObject ($productCollection | Sort-Object -Unique) -SyncWindow 0)) + { + Write-Verbose -Message $script:localizedData.ProductTestFailed + $testTargetResourceReturnValue = $false + } + } + + # Test Classifications + if ($PSBoundParameters.ContainsKey('Classifications')) { - Write-Verbose -Message $script:localizedData.ProxyCredTestFailed - return $false + if ($null -ne (Compare-Object -ReferenceObject ($Wsus.Classifications | Sort-Object -Unique) ` + -DifferenceObject ($Classifications | Sort-Object -Unique) -SyncWindow 0)) + { + Write-Verbose -Message $script:localizedData.ClassificationsTestFailed + $testTargetResourceReturnValue = $false + } } + } - if ($Wsus.ProxyServerBasicAuthentication -ne $ProxyServerBasicAuthentication) + # Test Synchronization Schedule + if ($PSBoundParameters.ContainsKey('SynchronizeAutomatically')) + { + if ($Wsus.SynchronizeAutomatically -ne $SynchronizeAutomatically) { - Write-Verbose -Message $script:localizedData.ProxyBasicAuthTestFailed - return $false + Write-Verbose -Message $script:localizedData.SyncAutomaticallyTestFailed + $testTargetResourceReturnValue = $false + } + if ($SynchronizeAutomatically) + { + if ($PSBoundParameters.ContainsKey('SynchronizeAutomaticallyTimeOfDay')) + { + if ($Wsus.SynchronizeAutomaticallyTimeOfDay -ne $SynchronizeAutomaticallyTimeOfDay) + { + Write-Verbose -Message $script:localizedData.SyncTimeOfDayTestFailed + $testTargetResourceReturnValue = $false + } + } + if ($PSBoundParameters.ContainsKey('SynchronizationsPerDay')) + { + if ($Wsus.SynchronizationsPerDay -ne $SynchronizationsPerDay) + { + Write-Verbose -Message $script:localizedData.SyncPerDayTestFailed + $testTargetResourceReturnValue = $false + } + } } } - else + + # If this is not a replica server + if (-not $UpstreamServerReplica) { - if ($null -ne $Wsus.ProxyServerCredentialUserName) + # Test Advanced Automatic Approvals + if ($PSBoundParameters.ContainsKey('AutoApproveWsusInfrastructureUpdates')) + { + if ($Wsus.AutoApproveWsusInfrastructureUpdates -ne $AutoApproveWsusInfrastructureUpdates) + { + Write-Verbose -Message $script:localizedData.UpdateFilesAutoApproveWsusInfraTestFailed + $testTargetResourceReturnValue = $false + } + } + if ($PSBoundParameters.ContainsKey('AutoRefreshUpdateApprovals')) { - Write-Verbose -Message $script:localizedData.ProxyCredSetTestFailed - return $false + if ($Wsus.AutoRefreshUpdateApprovals -ne $AutoRefreshUpdateApprovals) + { + Write-Verbose -Message $script:localizedData.UpdateFilesAutoRefreshUpdateApprovalsTestFailed + $testTargetResourceReturnValue = $false + } + if ($AutoRefreshUpdateApprovals) + { + if ($PSBoundParameters.ContainsKey('AutoRefreshUpdateApprovalsDeclineExpired')) + { + if ($Wsus.AutoRefreshUpdateApprovalsDeclineExpired -ne $AutoRefreshUpdateApprovalsDeclineExpired) + { + Write-Verbose -Message $script:localizedData.UpdateFilesAutoDeclineExpiredTestFailed + $testTargetResourceReturnValue = $false + } + } + } } } - } - # Test Languages - if ($Wsus.Languages.count -le 1 -and $Languages.count -le 1 -and $Languages -ne '*') - { - if ($Wsus.Languages -notmatch $Languages) + + # Test Client Targeting Mode + if ($PSBoundParameters.ContainsKey('ClientTargetingMode')) { - Write-Verbose -Message $script:localizedData.LanguageAsStrTestFailed - return $false + if ($Wsus.ClientTargetingMode -ne $ClientTargetingMode) + { + Write-Verbose -Message $script:localizedData.ClientTargetingModeTestFailed + $testTargetResourceReturnValue = $false + } } - } - else - { - if ($null -ne (Compare-Object -ReferenceObject ($Wsus.Languages | Sort-Object -Unique) ` - -DifferenceObject ($Languages | Sort-Object -Unique) -SyncWindow 0)) + + # If this is not a replica server + if (-not $UpstreamServerReplica) { - Write-Verbose -Message $script:localizedData.LanguageSetTestFailed - return $false + # Test Reporting rollup + if ($PSBoundParameters.ContainsKey('DoDetailedRollup')) + { + if ($Wsus.DoDetailedRollup -ne $DoDetailedRollup) + { + Write-Verbose -Message $script:localizedData.ReportingRollupDoDetailedRollupTestFailed + $testTargetResourceReturnValue = $false + } + } } - } - # Test Products - try - { - $wsusServer = Get-WsusServer -ErrorAction Stop - } - catch - { - Write-Verbose -Message $script:localizedData.TestGetWsusServer - return $false - } - $allWsusProducts = $wsusServer.GetUpdateCategories() - [System.Collections.ArrayList]$productCollection = @() - switch ($Products) - { - # All Products - '*' { - Write-Verbose -Message $script:localizedData.GetAllProductForTest - foreach ($prdct in $allWsusProducts) + # Test Email notifications + if ($PSBoundParameters.ContainsKey('SyncNotificationRecipients')) + { + if (($Wsus.SyncNotificationRecipients -isnot [Array] -and $Wsus.SyncNotificationRecipients -ne $SyncNotificationRecipients) -or + ($Wsus.SyncNotificationRecipients -is [Array] -and $null -ne (Compare-Object -ReferenceObject $Wsus.SyncNotificationRecipients ` + -DifferenceObject $SyncNotificationRecipients -SyncWindow 0))) { - $null = $productCollection.Add($prdct.Title) + Write-Verbose -Message $script:localizedData.SyncNotificationRecipientsTestFailed + $testTargetResourceReturnValue = $false } - continue } - # if Products property contains wild card like "Windows*" - {[System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($_)} { - $wildcardPrdct = $_ - Write-Verbose -Message $($script:localizedData.GetWildCardProductForTest -f $wildcardPrdct) - if ($wsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -like $wildcardPrdct }) + if ($PSBoundParameters.ContainsKey('StatusNotificationRecipients')) + { + if (($Wsus.StatusNotificationRecipients -isnot [Array] -and $Wsus.StatusNotificationRecipients -ne $StatusNotificationRecipients) -or + ($StatusNotificationRecipients -is [Array] -and $null -ne (Compare-Object -ReferenceObject $Wsus.StatusNotificationRecipients ` + -DifferenceObject $StatusNotificationRecipients -SyncWindow 0))) + { + Write-Verbose -Message $script:localizedData.StatusNotificationRecipientsTestFailed + $testTargetResourceReturnValue = $false + } + if ($StatusNotificationRecipients) { - foreach ($pdt in $wsusProduct) + if ($PSBoundParameters.ContainsKey('StatusNotificationFrequency')) { - $null = $productCollection.Add($pdt.Title) + if ($Wsus.StatusNotificationFrequency -ne $StatusNotificationFrequency) + { + Write-Verbose -Message $script:localizedData.StatusNotificationFrequencyTestFailed + $testTargetResourceReturnValue = $false + } + } + if ($PSBoundParameters.ContainsKey('StatusNotificationTimeOfDay')) + { + if ($Wsus.StatusNotificationTimeOfDay -ne $StatusNotificationTimeOfDay) + { + Write-Verbose -Message $script:localizedData.StatusNotificationTimeOfDayTestFailed + $testTargetResourceReturnValue = $false + } } } - else + } + if ($PSBoundParameters.ContainsKey('EmailLanguage')) + { + if ($Wsus.EmailLanguage -ne $EmailLanguage) { - Write-Verbose -Message $script:localizedData.NoWildcardProductFound + Write-Verbose -Message $script:localizedData.EmailLanguageTestFailed + $testTargetResourceReturnValue = $false } - continue } - - <# - We can try to add GUID support for product with : - - $StringGuid ="077e4982-4dd1-4d1f-ba18-d36e419971c1" - $ObjectGuid = [System.Guid]::New($StringGuid) - $IsEmptyGUID = $ObjectGuid -eq [System.Guid]::empty - - Maybe with function - #> - - default { - $prdct = $_ - Write-Verbose -Message $($script:localizedData.GetNameProductForTest -f $prdct) - if ($wsusProduct = $allWsusProducts | Where-Object -FilterScript { $_.Title -eq $prdct }) + if ($PSBoundParameters.ContainsKey('SmtpHostName')) + { + if ($Wsus.SmtpHostName -ne $SmtpHostName) { - foreach ($pdt in $wsusProduct) + Write-Verbose -Message $script:localizedData.SmtpHostNameTestFailed + $testTargetResourceReturnValue = $false + } + if ($SmtpHostName) + { + if ($Wsus.SmtpPort -ne $SmtpPort) { - $null = $ProductCollection.Add($pdt.Title) + Write-Verbose -Message $script:localizedData.SmtpPortTestFailed + $testTargetResourceReturnValue = $false } } - else + } + if ($PSBoundParameters.ContainsKey('SenderDisplayName')) + { + if ($Wsus.SenderDisplayName -ne $SenderDisplayName) { - Write-Verbose -Message $script:localizedData.NoNameProductFound + Write-Verbose -Message $script:localizedData.SenderDisplayNameTestFailed + $testTargetResourceReturnValue = $false + } + } + if ($PSBoundParameters.ContainsKey('SenderEmailAddress')) + { + if ($Wsus.SenderEmailAddress -ne $SenderEmailAddress) + { + Write-Verbose -Message $script:localizedData.SenderEmailAddressTestFailed + $testTargetResourceReturnValue = $false + } + } + if ($SmtpHostName) + { + if ($PSBoundParameters.ContainsKey('EmailServerCredential')) + { + if ($Wsus.SmtpUserName -ne $EmailServerCredential.UserName) + { + Write-Verbose -Message $script:localizedData.EmailServerCredTestFailed + $testTargetResourceReturnValue = $false + } } } - } - - - if ($null -ne (Compare-Object -ReferenceObject ($Wsus.Products | Sort-Object -Unique) ` - -DifferenceObject ($productCollection | Sort-Object -Unique) -SyncWindow 0)) - { - Write-Verbose -Message $script:localizedData.ProductTestFailed - return $false - } - # Test Classifications - if ($null -ne (Compare-Object -ReferenceObject ($Wsus.Classifications | Sort-Object -Unique) ` - -DifferenceObject ($Classifications | Sort-Object -Unique) -SyncWindow 0)) - { - Write-Verbose -Message $script:localizedData.ClassificationsTestFailed - return $false - } + # Test IIS dynamic compression + if ($PSBoundParameters.ContainsKey('IIsDynamicCompression')) + { + if ($Wsus.IIsDynamicCompression -ne $IIsDynamicCompression) + { + Write-Verbose -Message $script:localizedData.IIsDynamicCompressionTestFailed + $testTargetResourceReturnValue = $false + } + } - # Test Synchronization Schedule - if ($SynchronizeAutomatically) - { - if ($PSBoundParameters.ContainsKey('SynchronizeAutomaticallyTimeOfDay')) + # Test BITS download priority foreground + if ($PSBoundParameters.ContainsKey('BitsDownloadPriorityForeground')) { - if ($Wsus.SynchronizeAutomaticallyTimeOfDay -ne $SynchronizeAutomaticallyTimeOfDay) + if ($Wsus.BitsDownloadPriorityForeground -ne $BitsDownloadPriorityForeground) { - Write-Verbose -Message $script:localizedData.SyncTimeOfDayTestFailed - return $false + Write-Verbose -Message $script:localizedData.BitsDownloadPriorityForegroundTestFailed + $testTargetResourceReturnValue = $false } } - if ($Wsus.SynchronizationsPerDay -ne $SynchronizationsPerDay) + # Test local publishing + if ($PSBoundParameters.ContainsKey('LocalPublishingMaxCabSize')) { - Write-Verbose -Message $script:localizedData.SyncPerDayTestFailed - return $false + if ($Wsus.LocalPublishingMaxCabSize -ne $LocalPublishingMaxCabSize) + { + Write-Verbose -Message $script:localizedData.LocalPublishingMaxCabSizeTestFailed + $testTargetResourceReturnValue = $false + } } - } - # Test Client Targeting Mode - if ($ClientTargetingMode) - { - if ($PSBoundParameters.ContainsKey('ClientTargetingMode')) + # Test max simultaneous file downloads + if ($PSBoundParameters.ContainsKey('MaxSimultaneousFileDownloads')) { - if ($Wsus.ClientTargetingMode -ne $ClientTargetingMode) + if ($Wsus.MaxSimultaneousFileDownloads -ne $MaxSimultaneousFileDownloads) { - Write-Verbose -Message $script:localizedData.ClientTargetingModeTestFailed - return $false + Write-Verbose -Message $script:localizedData.MaxSimultaneousFileDownloadsTestFailed + $testTargetResourceReturnValue = $false } } } - return $true + return $testTargetResourceReturnValue } <# @@ -1102,22 +2249,33 @@ function Test-TargetResource Saves the WSUS configuration #> -function SaveWsusConfiguration +function Save-WsusConfiguration { + param( + [int]$Attempts = 30 + ) + $Count = 0 do { try { + Write-Verbose -Message ($script:localizedData.SavingWSUSConfiguration -f $Count) $WsusConfiguration.Save() $WsusConfigurationReady = $true } catch { $WsusConfigurationReady = $false + $Count++ Start-Sleep -Seconds 1 } } - until ($WsusConfigurationReady) + until ($WsusConfigurationReady -or $Count -gt $Attempts) + + if (-not $WsusConfigurationReady) + { + New-InvalidOperationException -Message ($script:localizedData.FailedToSaveWSUSConfiguration -f $Attempts) + } } diff --git a/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.schema.mof b/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.schema.mof index 3cb75f4..907d18e 100644 --- a/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.schema.mof +++ b/source/DSCResources/DSC_UpdateServicesServer/DSC_UpdateServicesServer.schema.mof @@ -4,7 +4,7 @@ class DSC_UpdateServicesServer : OMI_BaseResource [Key, Description("An enumerated value that describes if WSUS is configured. Default value is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; [Write, EmbeddedInstance("MSFT_Credential"), Description("Credential to be used to perform the initial configuration.")] String SetupCredential; [Write, Description("SQL Server for the WSUS database, omit for Windows Internal Database.")] String SQLServer; - [Write, Description("Folder for WSUS update files.")] String ContentDir; + [Write, Description("Folder for WSUS update files. Set as empty string ('') to download from Microsoft Update.")] String ContentDir; [Write, Description("Join the Microsoft Update Improvement Program.")] Boolean UpdateImprovementProgram; [Write, Description("Upstream WSUS server, omit for Microsoft Update.")] String UpstreamServerName; [Write, Description("Port of upstream WSUS server.")] UInt16 UpstreamServerPort; @@ -13,14 +13,36 @@ class DSC_UpdateServicesServer : OMI_BaseResource [Write, Description("Proxy server to use when synchronizing, omit for no proxy.")] String ProxyServerName; [Write, Description("Proxy server port.")] UInt16 ProxyServerPort; [Write, EmbeddedInstance("MSFT_Credential"), Description("Proxy server credential, omit for anonymous.")] String ProxyServerCredential; - [Read, Description("Proxy server credential username.")] String ProxyServerCredentialUsername; + [Read, Description("Proxy server credential username, in the format DOMAIN\\Username.")] String ProxyServerCredentialUsername; [Write, Description("Allow proxy server basic authentication.")] Boolean ProxyServerBasicAuthentication; + [Write, Description("Updates are downloaded only when they are approved.")] Boolean DownloadUpdateBinariesAsNeeded; + [Write, Description("Express installation packages should be downloaded.")] Boolean DownloadExpressPackages; + [Write, Description("Update binaries are downloaded from Microsoft Update instead of from the upstream server.")] Boolean GetContentFromMU; [Write, Description("Update languages, * for all.")] String Languages[]; [Write, Description("Update products, * for all.")] String Products[]; [Write, Description("Update classifications, * for all.")] String Classifications[]; [Write, Description("Synchronize automatically.")] Boolean SynchronizeAutomatically; - [Write, Description("First synchronization.")] String SynchronizeAutomaticallyTimeOfDay; - [Write, Description("Synchronizations per day.")] UInt16 SynchronizationsPerDay; + [Write, Description("First synchronization. The value must be a string representation of a TimeSpan value. The valid range is 00:00:00 to 23:59:59 inclusive (as UTC).")] String SynchronizeAutomaticallyTimeOfDay; + [Write, Description("Synchronizations per day. The valid range is 1 to 24 inclusive.")] UInt16 SynchronizationsPerDay; [Write, Description("Begin initial synchronization.")] Boolean Synchronize; - [write, Description("An enumerated value that describes if how the Target Groups are populated. Default value is 'Client'."), ValueMap{"Client","Server"}, Values{"Client","Server"}] String ClientTargetingMode; + [Write, Description("WSUS infrastructure updates are approved automatically.")] Boolean AutoApproveWsusInfrastructureUpdates; + [Write, Description("The latest revision of an update should be approved automatically.")] Boolean AutoRefreshUpdateApprovals; + [Write, Description("An update should be automatically declined when it is revised to be expired and AutoRefreshUpdateApprovals is enabled.")] Boolean AutoRefreshUpdateApprovalsDeclineExpired; + [Write, Description("An enumerated value that describes how the Target Groups are populated. Default value is 'Client'."), ValueMap{"Client","Server"}, Values{"Client","Server"}] String ClientTargetingMode; + [Write, Description("The downstream server should roll up detailed computer and update status information.")] Boolean DoDetailedRollup; + [Write, Description("E-mail addresses of those to whom notification of new updates should be sent, omit for no notifications.")] String SyncNotificationRecipients[]; + [Write, Description("The frequency with which e-mail notifications should be sent.\nDaily {default} \nWeekly \n"), ValueMap{"Daily","Weekly"}, Values{"Daily","Weekly"}] String StatusNotificationFrequency; + [Write, Description("The time of the day e-mail notifications should be sent. The value must be a string representation of a TimeSpan value. The valid range is 00:00:00 to 23:59:59 inclusive (as UTC).")] String StatusNotificationTimeOfDay; + [Write, Description("E-mail addresses of those to whom update status notification should be sent, omit for no notifications.")] String StatusNotificationRecipients[]; + [Write, Description("E-mail language setting.")] String EmailLanguage; + [Write, Description("The host name of the SMTP server.")] String SmtpHostName; + [Write, Description("The port number of the SMTP server.")] UInt16 SmtpPort; + [Write, Description("The display name of the e-mail sender.")] String SenderDisplayName; + [Write, Description("The e-mail address of the sender.")] String SenderEmailAddress; + [Write, EmbeddedInstance("MSFT_Credential"), Description("The e-mail server credential, omit for anonymous.")] String EmailServerCredential; + [Read, Description("The e-mail sender's username.")] String SmtpUserName; + [Write, Description("Use Xpress Encoding to compress update metadata. Results in significant bandwidth savings, at the expense of some CPU overhead.")] Boolean IIsDynamicCompression; + [Write, Description("Use foreground priority for BITS downloads to handle issues with proxy servers that do not correctly handle HTTP 1.1 range request.")] Boolean BitsDownloadPriorityForeground; + [Write, Description("The maximum .cab file size (in megabytes) that Local Publishing will create.")] UInt32 LocalPublishingMaxCabSize; + [Write, Description("The maximum number of concurrent update downloads.")] UInt32 MaxSimultaneousFileDownloads; }; diff --git a/source/DSCResources/DSC_UpdateServicesServer/en-US/DSC_UpdateServicesServer.strings.psd1 b/source/DSCResources/DSC_UpdateServicesServer/en-US/DSC_UpdateServicesServer.strings.psd1 index af1ad2a..20c4a29 100644 --- a/source/DSCResources/DSC_UpdateServicesServer/en-US/DSC_UpdateServicesServer.strings.psd1 +++ b/source/DSCResources/DSC_UpdateServicesServer/en-US/DSC_UpdateServicesServer.strings.psd1 @@ -2,46 +2,104 @@ ConvertFrom-StringData @' TestFailedAfterSet = Test-TargetResource returned false after calling set. GettingWsusServer = Getting WSUS server. +GetWsusServerFailed = Get-WsusServer failed to return a WSUS Server. The server may not yet have been configured. WSUSConfigurationFailed = WSUS configuration failed. WsusEnsureValue = WSUS server is {0}. GettingWsusConfig = Getting WSUS server configuration. +GettingWsusDatabaseConfig = Getting WSUS server database configuration. GettingWsusSubscription = Getting WSUS server subscription. +GettingWsusEmailNotificationConfig = Getting WSUS server email notification configuration. GettingWsusSQLServer = Getting WSUS SQL server. SQLServerName = WSUS Server SQL Server is {0}. -GetWSUSContentDir = Getting WSUS Server content directory. -WsusContentDir = WSUS Server content directory is {0}. GetWsusImproveProgram = Getting WSUS Server update improvement program. ImprovementProgram = WSUS Server content update improvement program is {0}. GetUpstreamServer = Getting WSUS upstream server. -UpstreamServer = WSUS Server upstream server is {0}, port {1}, use SSL {2}, replica {3}. +UpstreamServer = WSUS Server upstream server is {0}, port {1}, use SSL {2}. +GetReplicaServer = Getting WSUS replica server. +ReplicaServer = WSUS Server replica server is {0}. GetWsusProxyServer = Getting WSUS Server proxy server. -WsusProxyServer = WSUS Server proxy server is {0}, port {1}, basic authentication {2}. +WsusProxyServer = WSUS Server proxy server is {0}, port {1}, user name {2}, basic authentication {3}. +GettingWsusUpdateFiles = Getting WSUS Server update files. +WsusUpdateFiles = WSUS Server update files is content directory {0}, download binaries as needed {1}, download express packages {2}, get content from Microsoft Update {3}. GettingWsusLanguage = Getting WSUS Server languages. WsusLanguages = WSUS Server languages are {0}. GettingWsusClassifications = Getting WSUS Server Classifications. WsusClassifications = WSUS Server Classifications are {0}. GettingWsusProducts = Getting WSUS Server products. WsusProducts = WSUS Server products are {0}. +GettingWsusAdvancedAutomaticApprovals = Getting WSUS Server advanced automatic approvals. +WsusAutoApproveWsusInfrastructureUpdates = WSUS Server auto approve WSUS infrastructure updates is {0}. +WsusAutoRefreshUpdateApprovals = WSUS Server auto refresh update approvals is {0}. +WsusAutoRefreshUpdateApprovalsDeclineExpired = WSUS server auto decline when revised update is expired is {0}. GettingWsusSyncConfig = Getting WSUS Server synchronization settings. WsusSyncAuto = WSUS Server synchronize automatically is {0}. -WsusSyncAutoTimeOfDay = WSUS Server synchronize automatically time of day is {0}. +WsusSyncAutoTimeOfDay = WSUS Server synchronize automatically time of day is {0} (as UTC). WsusSyncPerDay = WSUS Server number of synchronizations per day is {0}. +GettingWsusTargetingMode = Getting WSUS Server targeting mode. WsusClientTargetingMode = WSUS Server client targeting mode is {0}. +GettingWsusReportingRollup = Getting WSUS Server reporting rollup. +WsusDoDetailedRollup = WSUS Server downstream server rollup is {0}. +GettingWsusEmailNotifications = Getting WSUS Server E-mail Notifications. +WsusSyncNotification = WSUS Server e-mail sync notification recipients are {0}. +WsusStatusNotification = WSUS Server e-mail status notification frequency is {0}, time of day {1} (as UTC), recipients {2}. +WsusEmailLanguage = WSUS Server e-mail language is {0}. +WsusSmtpHostName = WSUS Server e-mail SMTP host name is {0}. +WsusSmtpPort = WSUS Server e-mail SMTP port is {0}. +WsusSenderDisplayName = WSUS Server e-mail sender display name is {0}. +WsusSenderEmailAddress = WSUS Server e-mail sender address is {0}. +WsusSmtpServerUserName = WSUS Server e-mail SMTP server user name is {0}. +GettingWsusIIsDynamicCompression = Getting WSUS Server IIS dynamic compression. +IIsDynamicCompression = WSUS Server IIS dynamic compression is {0}. +GettingWsusBitsDownloadPriorityForeground = Getting WSUS Server BITS download priority foreground. +BitsDownloadPriorityForeground = WSUS Server BITS download priority foreground is {0}. +GettingWsusLocalPublishingMaxCabSize = Getting WSUS Server local publishing max CAB size. +WsusLocalPublishingMaxCabSize = WSUS Server local publishing max CAB size is {0}. +GettingWsusMaxSimultaneousFileDownloads = Getting WSUS Server max simultaneous file downloads. +WsusMaxSimultaneousFileDownloads = WSUS Server max simultaneous file downloads is {0}. RunningWsusPostInstall = Running WSUS Post Install tasks. ResolveWsusUtilExePath = WsusUtil.exe path is {0}. WsusUtilArgs = WsusUtil.exe {0}. ConfiguringWsus = Configuring WSUS. CheckPreviousConfig = Check for previous configuration change. -ConfiguringUpdateImprove= Configuring WSUS Update Improvement Program. -ConfiguringUpstreamServer= Configuring WSUS Upstream Server. -ConfiguringWsusMsftUpdates= Configuring WSUS for Microsoft Update. +ConfiguringUpdateImprove = Configuring WSUS Update Improvement Program. +ConfiguringUpstreamServer = Configuring WSUS Upstream Server. +ConfiguringWsusMsftUpdates = Configuring WSUS for Microsoft Update. +ConfiguringUpstreamServerReplica = Configuring WSUS Upstream Server Replica. +ConfiguringNoUpstreamServerReplica = Configuring WSUS No Upstream Server Replica. ConfiguringWsusProxy = Configuring WSUS proxy server. ConfiguringProxyCred = Configuring WSUS proxy server credential. RemovingProxyCred = Removing WSUS proxy server credential. ConfiguringNoProxy = Configuring WSUS no proxy server. +ConfiguringUpdateFiles = Configuring WSUS Update Files. +ConfiguringHostMUStore = Configuring WSUS to host binaries on Microsoft Update. +ConfiguringDownloadUpdateBinariesAsNeeded = Configuring WSUS download binaries as needed. +ConfiguringDownloadExpressPackages = Configuring WSUS download express packages. +ConfiguringGetContentFromMU = Configuring WSUS get content from Microsoft Update. ConfiguringLanguages = Setting WSUS languages. -ConfiguringClientTargetMode = Setting WSUS client targeting mode. -RemovingDefaultInit = Removing default products and classifications before initial sync. +ConfiguringAutoApproveWsusInfrastructureUpdates = Configuring WSUS auto approve WSUS infrastructure updates. +ConfiguringAutoRefreshUpdateApprovals = Configuring WSUS auto refresh update approvals. +ConfiguringAutoRefreshUpdateApprovalsDeclineExpired = Configuring WSUS auto decline when revised update is expired. +ConfiguringClientTargetMode = Configuring WSUS client targeting mode. +ConfiguringReportingRollup = Configuring WSUS reporting rollup. +ConfiguringSyncNotificationRecipients = Configuring WSUS e-mail sync notification recipients. +ConfiguringNoSyncNotificationRecipients = Configuring WSUS no e-mail sync notification recipients. +ConfiguringStatusNotificationRecipients = Configuring WSUS e-mail status notification recipients. +ConfiguringStatusNotificationFrequency = Configuring WSUS e-mail status notification frequency. +ConfiguringStatusNotificationTimeOfDay = Configuring WSUS e-mail status notification time of day. +ConfiguringNoStatusNotificationRecipients = Configuring WSUS no e-mail status notification recipients. +ConfiguringEmailLanguage = Configuring WSUS e-mail language. +UnsupportedEmailLanguage = Unsupported email language specified {0}. +ConfiguringSmtpHostName = Configuring SMTP host name and port. +ConfiguringSenderEmailAddress = Configuring SMTP sender email address. +ConfiguringSenderDisplayName = Configuring SMTP sender display name. +ConfiguringEmailServerCredential = Configuring WSUS e-mail server credential. +RemovingEmailServerCredential = Removing WSUS e-mail server credential. +ConfiguringIIsDynamicCompression = Configuring IIS dynamic compression. +ConfiguringBitsDownloadPriorityForeground = Configuring BITS download priority foreground. +ConfiguringLocalPublishing = Configuring local publishing. +ConfiguringMaxSimultaneousFileDownloads = Configuring max simultaneous file downloads +RemovingDefaultProductsInit = Removing default products before initial sync. +RemovingDefaultClassificationsInit = Removing default classifications before initial sync. RunningInitSync = Running WSUS initial synchronization online. InitSyncSuccess = Initial WSUS synchronization succeeded. InitSyncFailure = Initial WSUS synchronization failed. @@ -52,21 +110,45 @@ ConfiguringWildcardProducts = Configuring products for wilcard expression produc NoWildcardProductFound = No products found for wildcard expression product. ConfiguringNameProduct = Configuring products for product name : {0} NoNameProductFound = No product found for product name. -ConfiguringClassifications= Setting WSUS classifications. +ConfiguringClassifications = Setting WSUS classifications. ClassificationNotFound = Classification {0} not found. ConfiguringSyncSchedule = Setting WSUS synchronization schedule. +ConfiguringSyncTimeOfDay = Setting WSUS synchronization time of day. +ConfiguringSyncPerDay = Setting number of WSUS synchronizations per day. SynchronizingWsus = Synchronizing WSUS. EnsureTestFailed = Ensure test failed. -ImproveProgramTestFailed= Update Improvement Program test failed. +ImproveProgramTestFailed = Update Improvement Program test failed. UpstreamNameTestFailed = Upstream Server Name test failed. UpstreamPortTestFailed = Upstream Server Port test failed. UpstreamSSLTestFailed = Upstream Server SSL test failed. UpstreamReplicaTestFailed = Upstream Server Replica test failed. +NoUpstreamServerReplicaTestFailed = No Upstream Server Replica test failed. ProxyNameTestFailed = Proxy Server Name test failed. ProxyPortTestFailed = Proxy Server Port test failed. ProxyCredTestFailed = Proxy Server Credential test failed - incorrect credential. -ProxyBasicAuthTestFailed= Proxy Server Basic Authentication test failed. +ProxyBasicAuthTestFailed = Proxy Server Basic Authentication test failed. ProxyCredSetTestFailed = Proxy Server Credential test failed - credential set. +ContentDirTestFailed = Content dir test failed. +UpdateFilesDownloadUpdateBinariesTestFailed = Update Files download update binaries as needed test failed. +UpdateFilesDownloadExpressPackagesTestFailed = Update Files download express packages test failed. +UpdateFilesGetContentFromMUTestFailed = Update Files get content from Microsoft Update test failed. +UpdateFilesAutoApproveWsusInfraTestFailed = Update Files auto approve WSUS infrastructure updates test failed. +UpdateFilesAutoRefreshUpdateApprovalsTestFailed = Update Files auto refresh update approvals test failed. +UpdateFilesAutoDeclineExpiredTestFailed = Update Files auto refresh update approvals decline expired test failed. +SyncNotificationRecipientsTestFailed = Sync notification recipients test failed. +StatusNotificationRecipientsTestFailed = Status notification recipients test failed. +StatusNotificationFrequencyTestFailed = Status notification frequency test failed. +StatusNotificationTimeOfDayTestFailed = Status notification time of day test failed. +EmailLanguageTestFailed = Email language test failed. +SmtpHostNameTestFailed = SMTP host name test failed. +SmtpPortTestFailed = SMTP port test failed. +SenderDisplayNameTestFailed = Sender display name test failed. +SenderEmailAddressTestFailed = Sender e-mail address test failed. +EmailServerCredTestFailed = E-mail server Credential test failed - incorrect credential. +IIsDynamicCompressionTestFailed = IIS dynamic compression test failed. +BitsDownloadPriorityForegroundTestFailed = BITS download priority foreground test failed. +LocalPublishingMaxCabSizeTestFailed = Local publishing max CAB size test failed. +MaxSimultaneousFileDownloadsTestFailed = Max simultaneous file downloads test failed. LanguageAsStrTestFailed = Languages test failed (evaluated as single string). LanguageSetTestFailed = Languages test failed. TestGetWsusServer = Test Products failed, no WSUS Server found. @@ -74,8 +156,12 @@ GetAllProductForTest = Getting all products. GetWildCardProductForTest = Getting all products based on wildcard : {0} GetNameProductForTest = Getting products based on name : {0} ProductTestFailed = Products test failed. -ClassificationsTestFailed= Classifications test failed. +ClassificationsTestFailed = Classifications test failed. +SyncAutomaticallyTestFailed = Synchronize Automatically test failed. SyncTimeOfDayTestFailed = Synchronize Automatically Time Of Day test failed. SyncPerDayTestFailed = Synchronizations Per Day test failed. ClientTargetingModeTestFailed = Client Targeting Mode test failed. +ReportingRollupDoDetailedRollupTestFailed = Reporting rollup do detailed rollup test failed. +SavingWSUSConfiguration = Attempting to save WSUS configuration - attempt {0}. +FailedToSaveWSUSConfiguration = Failed to save WSUS configuration after {0} attempts. '@ From 182dfd577f60a6d8ba8711f847df23385353455d Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 17:06:38 +0000 Subject: [PATCH 06/12] Update CHANGELOG --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ca009..df3aec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- UpdateServicesServer + - BREAKING CHANGE: All parameters will now only be set when specifically applied + rather than defaulting to hardcoded values if left undefined. + In particular set ContentDir, Languages, Products, Classifications as needed. + Fixes [issue #55](https://github.com/dsccommunity/UpdateServicesDsc/issues/55) + - Updated initial offline package sync WSUS.cab. - Changed azure pipeline to use latest version of ubuntu and change the management of pipeline artifact @@ -36,6 +42,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- UpdateServicesServer + - Added support for the following settings: + - ContentDir can be set to empty string for clients to download from Microsoft Update. + - Updates are downloaded only when they are approved. + - Express installation packages should be downloaded. + - Update binaries are downloaded from Microsoft Update instead of from the + upstream server. + Fixes [issue #39](https://github.com/dsccommunity/UpdateServicesDsc/issues/39) + - WSUS infrastructure updates are approved automatically. + - The latest revision of an update should be approved automatically. + - An update should be automatically declined when it is revised to be expired + and AutoRefreshUpdateApprovals is enabled. + - The downstream server should roll up detailed computer and update status information. + - Email status notifications and SMTP settings, including status notifications DST fix. + Fixes [issue #15](https://github.com/dsccommunity/UpdateServicesDsc/issues/15) + - Use Xpress Encoding to compress update metadata. + - Use foreground priority for BITS downloads + - The maximum .cab file size (in megabytes) that Local Publishing will create. + - The maximum number of concurrent update downloads. - Added UpdateServicesComputerTargetGroup Resource to manage computer target groups ([issue #44](https://github.com/dsccommunity/UpdateServicesDsc/issues/44)) - Added TestKitchen files for integration tests @@ -45,6 +70,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- UpdateServicesApprovalRule + - Before running, ensure that UpdateServices PowerShell module is installed. + - Updated error handling to specifically catch errors if WSUS Server is unavailable. + - Added check to make sure Post Install was successful before trying to get resource. + - Fix issue [#64](https://github.com/dsccommunity/UpdateServicesDsc/issues/61) + Allow multiple product categories with same name (e.g. "Windows Admin Center") + - Removed ErrorRecord from New-InvalidOperationException outside of try / catch. +- UpdateServicesCleanup + - Fix issue [#93](https://github.com/dsccommunity/UpdateServicesDsc/issues/93) + Allow UpdateServicesCleanup resource to test and update TimeOfDay as needed. +- UpdateServicesComputerTargetGroup + - Before running, ensure that UpdateServices PowerShell module is installed. + - Updated error handling to specifically catch errors if WSUS Server is unavailable. + - Added check to make sure Post Install was successful before trying to get resource. +- UpdateServicesServer + - Before running, ensure that UpdateServices PowerShell module is installed. + - Updated error handling to specifically catch errors if WSUS Server is unavailable. + - Added check to make sure Post Install was successful before trying to get resource. + - Update setting dependency logic to stop incompatible settings being set / returned. + - Get Languages as a string array instead fo comma separated values. +- Stopped PDT.psm1 returning boolean 'true' alongside normal output as creating process. - Fix deploy job in AzurePipeline, Added Sampler.GithubTasks in build.yaml - Fix issue #61 and #67, with add a foreach loop when `Set-TargetResource` found multiple products for the same `Title`. From 29fcdaf4ab6b9128a48f915404590c91eb2adc10 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 17:06:55 +0000 Subject: [PATCH 07/12] Update CHANGELOG --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df3aec9..a5dca04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 rather than defaulting to hardcoded values if left undefined. In particular set ContentDir, Languages, Products, Classifications as needed. Fixes [issue #55](https://github.com/dsccommunity/UpdateServicesDsc/issues/55) - - Updated initial offline package sync WSUS.cab. - Changed azure pipeline to use latest version of ubuntu and change the management of pipeline artifact From 7ed0bce3b3d8804ac76cfaca1d2fc502365d51fa Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 17:12:37 +0000 Subject: [PATCH 08/12] Update CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5dca04..0024c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,7 +88,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated error handling to specifically catch errors if WSUS Server is unavailable. - Added check to make sure Post Install was successful before trying to get resource. - Update setting dependency logic to stop incompatible settings being set / returned. - - Get Languages as a string array instead fo comma separated values. + - Get Languages as a string array instead of comma separated values. + Fix issue [#76](https://github.com/dsccommunity/UpdateServicesDsc/issues/76) - Stopped PDT.psm1 returning boolean 'true' alongside normal output as creating process. - Fix deploy job in AzurePipeline, Added Sampler.GithubTasks in build.yaml - Fix issue #61 and #67, with add a foreach loop when `Set-TargetResource` found From 362350a0a865f9b993680c9721c7f45e9c70f2fa Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 17:15:25 +0000 Subject: [PATCH 09/12] Update CHANGELOG --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0024c3c..3446b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,7 +73,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Before running, ensure that UpdateServices PowerShell module is installed. - Updated error handling to specifically catch errors if WSUS Server is unavailable. - Added check to make sure Post Install was successful before trying to get resource. - - Fix issue [#64](https://github.com/dsccommunity/UpdateServicesDsc/issues/61) + - Fix issue [#63](https://github.com/dsccommunity/UpdateServicesDsc/issues/63) + Broken verbose output for WSUS server name. + - Fix issue [#61](https://github.com/dsccommunity/UpdateServicesDsc/issues/61) Allow multiple product categories with same name (e.g. "Windows Admin Center") - Removed ErrorRecord from New-InvalidOperationException outside of try / catch. - UpdateServicesCleanup From 919beed74889dac7a159f217e6555044afa51051 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 17:36:03 +0000 Subject: [PATCH 10/12] Updated CHANGELOG --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3446b24..2b3d833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,9 +90,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated error handling to specifically catch errors if WSUS Server is unavailable. - Added check to make sure Post Install was successful before trying to get resource. - Update setting dependency logic to stop incompatible settings being set / returned. - - Get Languages as a string array instead of comma separated values. + - Get Languages as a string array instead of comma-separated values. Fix issue [#76](https://github.com/dsccommunity/UpdateServicesDsc/issues/76) -- Stopped PDT.psm1 returning boolean 'true' alongside normal output as creating process. +- Stopped PDT.psm1 returning boolean 'true' alongside normal output when creating a process. - Fix deploy job in AzurePipeline, Added Sampler.GithubTasks in build.yaml - Fix issue #61 and #67, with add a foreach loop when `Set-TargetResource` found multiple products for the same `Title`. From 114456439f20a84906785558de8c5a630744d9ad Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 17:38:30 +0000 Subject: [PATCH 11/12] Fixed verbose logging for languages --- .../DSC_UpdateServicesApprovalRule.psm1 | 4 ++-- .../en-US/DSC_UpdateServicesApprovalRule.strings.psd1 | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 b/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 index b9d3ced..fd24946 100644 --- a/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 +++ b/source/DSCResources/DSC_UpdateServicesApprovalRule/DSC_UpdateServicesApprovalRule.psm1 @@ -68,7 +68,7 @@ function Get-TargetResource (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Update Services\Server\Setup\Installed Role Services" ` -Name 'UpdateServices-Services' -ErrorAction Stop).'UpdateServices-Services' -eq '2') { - Write-Verbose -Message ('Identified WSUS server information: {0}' -f $WsusServer.Name) + Write-Verbose -Message ($script:localizedData.IdentifiedWsusServer -f $WsusServer.Name) $ApprovalRule = $WsusServer.GetInstallApprovalRules() | Where-Object -FilterScript { $_.Name -eq $Name } @@ -96,7 +96,7 @@ function Get-TargetResource } else { - Write-Verbose -Message 'Did not identify an instance of WSUS' + Write-Verbose -Message $script:localizedData.NotIdentifiedWsusServer } } catch diff --git a/source/DSCResources/DSC_UpdateServicesApprovalRule/en-US/DSC_UpdateServicesApprovalRule.strings.psd1 b/source/DSCResources/DSC_UpdateServicesApprovalRule/en-US/DSC_UpdateServicesApprovalRule.strings.psd1 index 1de0142..0bae9ec 100644 --- a/source/DSCResources/DSC_UpdateServicesApprovalRule/en-US/DSC_UpdateServicesApprovalRule.strings.psd1 +++ b/source/DSCResources/DSC_UpdateServicesApprovalRule/en-US/DSC_UpdateServicesApprovalRule.strings.psd1 @@ -6,6 +6,8 @@ RunApprovalRule = Running approval rule {0}. SyncWsus = Synchronizing WSUS. ClassificationNotFound = Classification {0} not found. GetWsusServerFailed = Get-WsusServer failed. +IdentifiedWsusServer = Identified WSUS server information: {0} +NotIdentifiedWsusServer = Did not identify an instance of WSUS WSUSConfigurationFailed = WSUS approval rule configuration failed. RuleFailedToCreate = Failed to create approval rule {0}. RuleFailedToApply = Failed to apply approval rule {0}. From e7f513f84d70055953c6b3e0e9f42f1282eecb6e Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 18 Nov 2025 17:39:07 +0000 Subject: [PATCH 12/12] Updated CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3d833..1fec030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix issue [#61](https://github.com/dsccommunity/UpdateServicesDsc/issues/61) Allow multiple product categories with same name (e.g. "Windows Admin Center") - Removed ErrorRecord from New-InvalidOperationException outside of try / catch. + - Fixed verbose logging to use language strings. - UpdateServicesCleanup - Fix issue [#93](https://github.com/dsccommunity/UpdateServicesDsc/issues/93) Allow UpdateServicesCleanup resource to test and update TimeOfDay as needed.