diff --git a/CHANGELOG.md b/CHANGELOG.md index ceb8151..b979a57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added UpdateServicesComputerTargetGroup Resource to manage computer target groups + ### Changed - Updated initial offline package sync WSUS.cab. @@ -20,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated to load localization strings correctly. - UpdateServicesServer - Updated to load localization strings correctly. +- Updated ImitateUpdateServicesModule Module to meet style guidelines - Internal PDT helper module - Updated to load localization strings correctly. - General code cleanup @@ -44,6 +49,8 @@ multiple products for the same `Title`. - Fix issue [#63](https://github.com/dsccommunity/UpdateServicesDsc/issues/63), Fixed verbose output of WSUS server in UpdateServicesApprovalRule - Fixed the `azure-pipelines.yml` to trigger on main not master. +- Update build process to pin GitVersion to 5.* to resolve errors + (https://github.com/gaelcolas/Sampler/issues/477). ## [1.2.0] - 2020-05-18 diff --git a/ReadMe.md b/ReadMe.md index c84ed5c..896b78c 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -86,6 +86,12 @@ Windows Server 2008 R2 SP1, Windows Server 2012 and Windows Server 2012 R2. * **CleanupLocalPublishedContentFiles**: Cleanup local published content files. * **TimeOfDay** Time of day to start cleanup. +**UpdateServicesComputerTargetGroup** resource has following properties: + +* **Ensure**: An enumerated value that describes if the Computer Target Group exists. +* **Name**: Name of the Computer Target Group. +* **Path**: Path to the Computer Target Group in the format 'Parent/Child'. + **UpdateServicesServer** resource has following properties: * **Ensure**: An enumerated value that describes if WSUS is configured. diff --git a/Tests/Helpers/ImitateUpdateServicesModule.psm1 b/Tests/Helpers/ImitateUpdateServicesModule.psm1 index 74dd5e2..7309c53 100644 --- a/Tests/Helpers/ImitateUpdateServicesModule.psm1 +++ b/Tests/Helpers/ImitateUpdateServicesModule.psm1 @@ -2,7 +2,7 @@ function Get-WsusServerTemplate { $WsusServer = [pscustomobject] @{ Name = 'ServerName' - } + } $ApprovalRule = [scriptblock]{ $ApprovalRule = [pscustomobject]@{ @@ -52,13 +52,134 @@ function Get-WsusServerTemplate return $ApprovalRule } + $ComputerTargetGroups = [scriptblock]{ + $ComputerTargetGroups = @( + [pscustomobject] @{ + Name = 'All Computers' + Id = [pscustomobject] @{ + GUID = '4be27a8d-b969-4a8a-9cae-ec6b3a282b0b' + } + }, + [pscustomobject] @{ + Name = 'Servers' + Id = [pscustomobject] @{ + GUID = '14adceba-ddf3-4299-9c1a-e4cf8bd56c47' + } + ParentTargetGroup = [pscustomobject] @{ + Name = 'All Computers' + Id = [pscustomobject] @{ + GUID = '4be27a8d-b969-4a8a-9cae-ec6b3a282b0b' + } + } + ChildTargetGroup = [pscustomobject] @{ + Name = 'Web' + Id = [pscustomobject] @{ + GUID = 'f4aa59c7-e6a0-4e6d-97b0-293d00a0dc60' + } + } + }, + [pscustomobject] @{ + Name = 'Web' + Id = [pscustomobject] @{ + GUID = 'f4aa59c7-e6a0-4e6d-97b0-293d00a0dc60' + } + ParentTargetGroup = [pscustomobject] @{ + Name = 'Servers' + Id = [pscustomobject] @{ + GUID = '14adceba-ddf3-4299-9c1a-e4cf8bd56c47' + } + ParentTargetGroup = [pscustomobject] @{ + Name = 'All Computers' + Id = [pscustomobject] @{ + GUID = '4be27a8d-b969-4a8a-9cae-ec6b3a282b0b' + } + } + } + }, + [pscustomobject] @{ + Name = 'Workstations' + Id = [pscustomobject] @{ + GUID = '31742fd8-df6f-4836-82b4-b2e52ee4ba1b' + } + ParentTargetGroup = [pscustomobject] @{ + Name = 'All Computers' + Id = [pscustomobject] @{ + GUID = '4be27a8d-b969-4a8a-9cae-ec6b3a282b0b' + } + } + }, + [pscustomobject] @{ + Name = 'Desktops' + Id = [pscustomobject] @{ + GUID = '2b77a9ce-f320-41c7-bec7-9b22f67ae5b1' + } + ParentTargetGroup = [pscustomobject] @{ + Name = 'Workstations' + Id = [pscustomobject] @{ + GUID = '31742fd8-df6f-4836-82b4-b2e52ee4ba1b' + } + ParentTargetGroup = [pscustomobject] @{ + Name = 'All Computers' + Id = [pscustomobject] @{ + GUID = '4be27a8d-b969-4a8a-9cae-ec6b3a282b0b' + } + } + } + } + ) + + foreach ($ComputerTargetGroup in $ComputerTargetGroups) + { + Add-Member -InputObject $ComputerTargetGroup -MemberType ScriptMethod -Name Delete -Value {} + + Add-Member -InputObject $ComputerTargetGroup -MemberType ScriptMethod -Name GetParentTargetGroup -Value { + return $this.ParentTargetGroup + } + + if ($null -ne $ComputerTargetGroup.ParentTargetGroup) + { + Add-Member -InputObject $ComputerTargetGroup.ParentTargetGroup -MemberType ScriptMethod -Name GetParentTargetGroup -Value { + return $this.ParentTargetGroup + } + } + + if ($null -ne $ComputerTargetGroup.ChildTargetGroup) + { + Add-Member -InputObject $ComputerTargetGroup -MemberType ScriptMethod -Name GetChildTargetGroups -Value { + return $this.ChildTargetGroup + } + + Add-Member -InputObject $ComputerTargetGroup.ChildTargetGroup -MemberType ScriptMethod -Name Delete -Value {} + } + } + + return $ComputerTargetGroups + } + + $WsusServer | Add-Member -MemberType ScriptMethod -Name CreateComputerTargetGroup -Value { + param + ( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [object] + $ComputerTargetGroup + ) + { + Write-Output $Name + Write-Output $ComputerTargetGroup + } + } + $WsusServer | Add-Member -MemberType ScriptMethod -Name GetInstallApprovalRules -Value $ApprovalRule $WsusServer | Add-Member -MemberType ScriptMethod -Name CreateInstallApprovalRule -Value $ApprovalRule $WsusServer | Add-Member -MemberType ScriptMethod -Name GetUpdateClassification -Value {} - $WsusServer | Add-Member -MemberType ScriptMethod -Name GetComputerTargetGroups -Value {} + $WsusServer | Add-Member -MemberType ScriptMethod -Name GetComputerTargetGroups -Value $ComputerTargetGroups $WsusServer | Add-Member -MemberType ScriptMethod -Name DeleteInstallApprovalRule -Value {} diff --git a/Tests/Unit/MSFT_UpdateServicesComputerTargetGroup.tests.ps1 b/Tests/Unit/MSFT_UpdateServicesComputerTargetGroup.tests.ps1 new file mode 100644 index 0000000..0f762c7 --- /dev/null +++ b/Tests/Unit/MSFT_UpdateServicesComputerTargetGroup.tests.ps1 @@ -0,0 +1,291 @@ +$script:dscModuleName = 'UpdateServicesDsc' +$script:dscResourceName = 'MSFT_UpdateServicesComputerTargetGroup' + +#region HEADER +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) + +Import-Module -Name DscResource.Test -Force -ErrorAction Stop + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType Unit + +#region Pester Test Initialization +Import-Module $PSScriptRoot\..\Helpers\ImitateUpdateServicesModule.psm1 -force -ErrorAction Stop + +#endregion HEADER + + +# Begin Testing +try +{ + InModuleScope $script:DSCResourceName { + + #region Function Get-ComputerTargetGroupPath + Describe "MSFT_UpdateServicesComputerTargetGroup\Get-ComputerTargetGroupPath." { + $WsusServer = Get-WsusServer + + Context "The Function returns expected path for the 'All Computers' ComputerTargetGroup." { + $ComputerTargetGroup = $WsusServer.GetComputerTargetGroups() | Where-Object -FilterScript { $_.Name -eq 'All Computers' } + $result = Get-ComputerTargetGroupPath -ComputerTargetGroup $ComputerTargetGroup + $result | Should -Be 'All Computers' + } + + Context "The Function returns expected path for the 'Desktops' ComputerTargetGroup." { + $ComputerTargetGroup = $WsusServer.GetComputerTargetGroups() | Where-Object -FilterScript { $_.Name -eq 'Desktops' } + $result = Get-ComputerTargetGroupPath -ComputerTargetGroup $ComputerTargetGroup + $result | Should -Be 'All Computers/Workstations' + } + + Context "The Function returns expected path for the 'Workstations' ComputerTargetGroup." { + $ComputerTargetGroup = $WsusServer.GetComputerTargetGroups() | Where-Object -FilterScript { $_.Name -eq 'Workstations' } + $result = Get-ComputerTargetGroupPath -ComputerTargetGroup $ComputerTargetGroup + $result | Should -Be 'All Computers' + } + } + #endregion + + #region Function Get-TargetResource + Describe "MSFT_UpdateServicesComputerTargetGroup\Get-TargetResource." { + BeforeEach { + if (Test-Path -Path variable:script:resource) { Remove-Variable -Scope 'script' -Name 'resource' } + } + + BeforeAll { + Mock -CommandName Write-Verbose -MockWith {} + } + + Context 'An error occurs retrieving WSUS Server configuration information.' { + Mock -CommandName Get-WsusServer -MockWith { throw 'An error occurred.' } + + It 'Calling Get should throw when an error occurs retrieving WSUS Server information.' { + { $script:resource = Get-TargetResource -Name 'Servers' -Path 'All Computers'} | Should -Throw ($script:localizedData.WSUSConfigurationFailed) + $script:resource | Should -Be $null + Assert-MockCalled -CommandName Get-WsusServer -Exactly 1 + } + } + + Context 'The WSUS Server is not yet configured.' { + Mock -CommandName Get-WsusServer -MockWith {} + + It 'Calling Get should not throw when the WSUS Server is not yet configured / cannot be found.' { + { $script:resource = Get-TargetResource -Name 'Servers' -Path 'All Computers'} | Should -Not -Throw + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq $script:localizedData.GetWsusServerFailed + } + $script:resource.Ensure | Should -Be 'Absent' + $script:resource.Id | Should -Be $null + $script:resource.Name | Should -Be 'Servers' + $script:resource.Path | Should -Be 'All Computers' + } + } + + Context 'The Computer Target Group is not in the desired state (specified name does not exist at any path).' { + It 'Calling Get should return absent when Computer Target Group does not exist at any path.' { + $resource = Get-TargetResource -Name 'Domain Controllers' -Path 'All Computers' + $resource.Ensure | Should -Be 'Absent' + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.GetWsusServerSucceeded -f 'ServerName') + } + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.NotFoundComputerTargetGroup -f 'Domain Controllers', 'All Computers') + } + + } + } + + Context 'The Computer Target Group is not in the desired state (specified name exists but not at the desired path).' { + It 'Calling Get should throw when Computer Target Group does not exist at the specified path.' { + { $script:resource = Get-TargetResource -Name 'Desktops' -Path 'All Computers/Servers' } | Should -Throw ` + ($script:localizedData.DuplicateComputerTargetGroup -f 'Desktops', 'All Computers/Workstations') + $script:resource | Should -Be $null + } + } + + Context 'The Computer Target Group is in the desired state (specified name exists with the desired path).' { + It 'Calling Get should return present when Computer Target Group does exist at the specified path.' { + $resource = Get-TargetResource -Name 'Desktops' -Path 'All Computers/Workstations' + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.FoundComputerTargetGroup -f ` + 'Desktops', 'All Computers/Workstations', '2b77a9ce-f320-41c7-bec7-9b22f67ae5b1') + } + $resource.Ensure | Should -Be 'Present' + $resource.Id | Should -Be '2b77a9ce-f320-41c7-bec7-9b22f67ae5b1' + + } + } + } + #endregion + + #region Function Test-TargetResource + Describe "MSFT_UpdateServicesComputerTargetGroup\Test-TargetResource." { + BeforeAll { + Mock -CommandName Write-Verbose -MockWith {} + } + + Context 'The Computer Target Group "Desktops" is "Present" at Path "All Computers/Workstations" which is the desired state.' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = 'Present' + Name = 'Desktops' + Path = 'All Computers/Workstations' + Id = '2b77a9ce-f320-41c7-bec7-9b22f67ae5b1' + } + } + + It 'Test-TargetResource should return $true when Computer Target Resource is in the desired state.' { + $resource = Test-TargetResource -Name 'Desktops' -Path 'All Computers/Workstations' + $resource | Should -Be $true + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.ResourceInDesiredState -f ` + 'Desktops', 'All Computers/Workstations', 'Present') + } + } + } + + Context 'The Computer Target Group "Desktops" is "Absent" at Path "All Computers/Workstations" which is the desired state (Present).' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = 'Absent' + Name = 'Desktops' + Path = 'All Computers/Workstations' + Id = $null + } + } + + It 'Test-TargetResource should return $true when Computer Target Resource is in the desired state.' { + $resource = Test-TargetResource -Name 'Desktops' -Path 'All Computers/Workstations' -Ensure 'Absent' + $resource | Should -Be $true + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.ResourceInDesiredState -f ` + 'Desktops', 'All Computers/Workstations', 'Absent') + } + } + } + + Context 'The Computer Target Group "Desktops" is "Present" at Path "All Computers/Workstations" which is NOT the desired state.' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = 'Present' + Name = 'Desktops' + Path = 'All Computers/Workstations' + Id = '2b77a9ce-f320-41c7-bec7-9b22f67ae5b1' + } + } + + It 'Test-TargetResource should return $false when Computer Target Resource is NOT in the desired state.' { + $resource = Test-TargetResource -Name 'Desktops' -Path 'All Computers/Workstations' -Ensure 'Absent' + $resource | Should -Be $false + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.ResourceNotInDesiredState -f ` + 'Desktops', 'All Computers/Workstations', 'Present') + } + } + } + + Context 'The Computer Target Group "Desktops" is "Absent" at Path "All Computers/Workstations" which is NOT the desired state (Present).' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + Ensure = 'Absent' + Name = 'Desktops' + Path = 'All Computers/Workstations' + Id = $null + } + } + + It 'Test-TargetResource should return $false when Computer Target Resource is NOT in the desired state.' { + $resource = Test-TargetResource -Name 'Desktops' -Path 'All Computers/Workstations' -Ensure 'Present' + $resource | Should -Be $false + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.ResourceNotInDesiredState -f ` + 'Desktops', 'All Computers/Workstations', 'Absent') + } + } + } + } + #endregion + + #region Function Set-TargetResource + Describe "MSFT_UpdateServicesComputerTargetGroup\Set-TargetResource" { + BeforeEach { + if (Test-Path -Path variable:script:resource) { Remove-Variable -Scope 'script' -Name 'resource' } + } + + BeforeAll { + Mock -CommandName Write-Verbose -MockWith {} + } + + Context 'An error occurs retrieving WSUS Server configuration information.' { + Mock -CommandName Get-WsusServer -MockWith { throw 'An error occurred.' } + + It 'Calling Set should throw when an error occurs retrieving WSUS Server information.' { + { $script:resource = Set-TargetResource -Name 'Servers' -Path 'All Computers'} | Should -Throw ($script:localizedData.WSUSConfigurationFailed) + $script:resource | Should -Be $null + Assert-MockCalled -CommandName Get-WsusServer -Exactly 1 + } + } + + Context 'The WSUS Server is not yet configured.' { + Mock -CommandName Get-WsusServer -MockWith {} + + It 'Calling Set should not throw when the WSUS Server is not yet configuration / cannot be found.' { + { $script:resource = Set-TargetResource -Name 'Servers' -Path 'All Computers'} | Should -Not -Throw + Assert-MockCalled -CommandName Write-Verbose -ParameterFilter { + $message -eq $script:localizedData.GetWsusServerFailed + } + $script:resource | Should -Be $null + } + } + + Context 'The Parent of the Computer Target Group is not present and therefore the new group cannot be created.' { + Mock -CommandName Write-Warning -MockWith {} + + It 'Calling Set where the Parent of the Computer Target Group does not exist throws an exception.' { + { $script:resource = Set-TargetResource -Name 'Win10' -Path 'All Computers/Desktops'} | Should -Throw ` + ($script:localizedData.NotFoundParentComputerTargetGroup -f 'Desktops', ` + 'All Computers', 'Win10') + } + } + + Context 'The new Computer Target Group (at Root Level) is successfully created.' { + It 'Calling Set where Computer Target Group (at Root Level) does not exist and Ensure is "Present" creates the required group.' { + { $script:resource = Set-TargetResource -Name 'Member Servers' -Path 'All Computers'} | Should -Not -Throw + Assert-MockCalled Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.CreateComputerTargetGroupSuccess -f 'Member Servers', ` + 'All Computers') + } + } + } + + Context 'The new Computer Target Group is successfully created.' { + It 'Calling Set where Computer Target Group does not exist and Ensure is "Present" creates the required group.' { + { $script:resource = Set-TargetResource -Name 'Database' -Path 'All Computers/Servers'} | Should -Not -Throw + Assert-MockCalled Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.CreateComputerTargetGroupSuccess -f 'Database', ` + 'All Computers/Servers') + } + } + } + + Context 'The new Computer Target Group is successfully deleted.' { + It 'Calling Set where Computer Target Group exists and Ensure is "Absent" deletes the required group.' { + { $script:resource = Set-TargetResource -Name 'Web' -Path 'All Computers/Servers' -Ensure 'Absent' } | Should -Not -Throw + Assert-MockCalled Write-Verbose -ParameterFilter { + $message -eq ($script:localizedData.DeleteComputerTargetGroupSuccess -f 'Web', ` + 'f4aa59c7-e6a0-4e6d-97b0-293d00a0dc60', 'All Computers/Servers') + } + } + } + } + #endregion + } +} + +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9160e45..7a18eda 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -26,7 +26,7 @@ stages: vmImage: 'ubuntu-latest' steps: - pwsh: | - dotnet tool install --global GitVersion.Tool + dotnet tool install --global GitVersion.Tool --version 5.* $gitVersionObject = dotnet-gitversion | ConvertFrom-Json $gitVersionObject.PSObject.Properties.ForEach{ Write-Host -Object "Setting Task Variable '$($_.Name)' with value '$($_.Value)'." diff --git a/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/MSFT_UpdateServicesComputerTargetGroup.psm1 b/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/MSFT_UpdateServicesComputerTargetGroup.psm1 new file mode 100644 index 0000000..b59468c --- /dev/null +++ b/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/MSFT_UpdateServicesComputerTargetGroup.psm1 @@ -0,0 +1,311 @@ +# DSC resource to manage WSUS Computer Target Groups. + +# Load Common Module +$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' +Import-Module -Name $script:resourceHelperModulePath +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + + +<# + .SYNOPSIS + Retrieves the current state of the WSUS Computer Target Group. + + The returned object provides the following properties: + Name: The Name of the WSUS Computer Target Group. + Path: The Path to the Parent of the Computer Target Group. + Id: The Id / GUID of the WSUS Computer Target Group. + .PARAMETER Name + The Name of the WSUS Computer Target Group. + + .PARAMETER Path + The Path to the WSUS Compter Target Group in the format 'Parent/Child'. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + try + { + $WsusServer = Get-WsusServer + $Ensure = 'Absent' + $Id = $null + + if ($null -ne $WsusServer) + { + Write-Verbose -Message ($script:localizedData.GetWsusServerSucceeded -f $WsusServer.Name) + $ComputerTargetGroup = $WsusServer.GetComputerTargetGroups() | Where-Object -FilterScript { $_.Name -eq $Name } + + if ($null -ne $ComputerTargetGroup) + { + $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 + } + } + catch + { + New-InvalidOperationException -Message $script:localizedData.WSUSConfigurationFailed -ErrorRecord $_ + } + + if ($null -eq $Id) + { + Write-Verbose -Message ($script:localizedData.NotFoundComputerTargetGroup -f $Name, $Path) + } + + $returnValue = @{ + Ensure = $Ensure + Name = $Name + Path = $Path + Id = $Id + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the state of the WSUS Computer Target Group. + + .PARAMETER Ensure + Determines if the Computer Target Group should be created or removed. + Accepts 'Present' (default) or 'Absent'. + + .PARAMETER Name + Name of the Computer Target Group. + + .PARAMETER Path + The Path to the Computer Target Group in the format 'Parent/Child'. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + try + { + $WsusServer = Get-WsusServer + + # 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-Object -FilterScript { + $_.Name -eq $ParentComputerTargetGroupName + } + + if ($null -ne $ParentComputerTargetGroups) + { + foreach ($ParentComputerTargetGroup in $ParentComputerTargetGroups) + { + $ComputerTargetGroupPath = Get-ComputerTargetGroupPath -ComputerTargetGroup $ParentComputerTargetGroup + if ($ParentComputerTargetGroupPath -eq $ComputerTargetGroupPath) + { + # 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') + { + try + { + $WsusServer.CreateComputerTargetGroup($Name, $ParentComputerTargetGroup) | Out-Null + Write-Verbose -Message ($script:localizedData.CreateComputerTargetGroupSuccess -f $Name, $Path) + return + } + catch + { + 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 + try + { + $ChildComputerTargetGroup = $ParentComputerTargetGroup.GetChildTargetGroups() | Where-Object -FilterScript { + $_.Name -eq $Name + } + $ChildComputerTargetGroup.Delete() | Out-Null + Write-Verbose -Message ($script:localizedData.DeleteComputerTargetGroupSuccess -f $Name, ` + $ChildComputerTargetGroup.Id.Guid, $Path) + return + } + catch + { + New-InvalidOperationException -Message ( + $script:localizedData.DeleteComputerTargetGroupFailed -f $Name, ` + $ChildComputerTargetGroup.Id.Guid, $Path + ) -ErrorRecord $_ + } + } + } + } + } + + New-InvalidOperationException -Message ($script:localizedData.NotFoundParentComputerTargetGroup -f $ParentComputerTargetGroupName, ` + $ParentComputerTargetGroupPath, $Name) + } + else + { + Write-Verbose -Message $script:localizedData.GetWsusServerFailed + } + } + catch + { + New-InvalidOperationException -Message $script:localizedData.WSUSConfigurationFailed -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Tests the current state of the WSUS Computer Target Group. + + .PARAMETER Ensure + Determines if the Computer Target Group should be created or removed. + Accepts 'Present' (default) or 'Absent'. + + .PARAMETER Name + Name of the Computer Target Group + + .PARAMETER Path + The Path to the Computer Target Group in the format 'Parent/Child'. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + $result = Get-TargetResource -Name $Name -Path $Path + + if ($Ensure -eq $result.Ensure) + { + Write-Verbose -Message ($script:localizedData.ResourceInDesiredState -f $Name, $Path, $result.Ensure) + return $true + } + else + { + Write-Verbose -Message ($script:localizedData.ResourceNotInDesiredState -f $Name, $Path, $result.Ensure) + return $false + } +} + + +<# + .SYNOPSIS + Gets the Computer Target Group Path within WSUS by recursing up through each Parent Computer Target Group + + .PARAMETER ComputerTargetGroup + The Computer TargetGroup +#> +function Get-ComputerTargetGroupPath +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [object] + $ComputerTargetGroup + ) + + if ($ComputerTargetGroup.Name -eq 'All Computers') + { + return "All Computers" + } + + $computerTargetGroupPath = "" + $computerTargetGroupParents = @() + $moreParentContainers = $true + $x = 0 + + do + { + try + { + $ComputerTargetGroup = $ComputerTargetGroup.GetParentTargetGroup() + $computerTargetGroupParents += $ComputerTargetGroup.Name + } + catch + { + # 'All Computers' container throws an exception when GetParentTargetGroup() method called + $moreParentContainers = $false + } + + $x++ + } while ($moreParentContainers -and ($x -lt 20)) + + for ($i=($computerTargetGroupParents.Count - 1); $i -ge 0; $i--) + { + if ("" -ne $computerTargetGroupPath) + { + $computerTargetGroupPath += ("/" + $computerTargetGroupParents[$i]) + } + else + { + $computerTargetGroupPath += $computerTargetGroupParents[$i] + } + } + + return $computerTargetGroupPath +} + +Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/MSFT_UpdateServicesComputerTargetGroup.schema.mof b/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/MSFT_UpdateServicesComputerTargetGroup.schema.mof new file mode 100644 index 0000000..a37c260 --- /dev/null +++ b/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/MSFT_UpdateServicesComputerTargetGroup.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("UpdateServicesComputerTargetGroup")] +class MSFT_UpdateServicesComputerTargetGroup : OMI_BaseResource +{ + [Write, Description("An enumerated value that describes if the WSUS Computer Target Group is configured.\nPresent {default} \nAbsent \n"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Key, Description("Name of the Computer Target Group.")] String Name; + [Key, Description("Path to the Computer Target Group.")] String Path; + [Read, Description("ID / GUID of the Computer Target Group.")] String Id; +}; diff --git a/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/en-US/MSFT_UpdateServicesComputerTargetGroup.strings.psd1 b/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/en-US/MSFT_UpdateServicesComputerTargetGroup.strings.psd1 new file mode 100644 index 0000000..173bc03 --- /dev/null +++ b/source/DSCResources/MSFT_UpdateServicesComputerTargetGroup/en-US/MSFT_UpdateServicesComputerTargetGroup.strings.psd1 @@ -0,0 +1,17 @@ +# Localized Strings for UpdateServicesApprovalRule resource +ConvertFrom-StringData @' +GetWsusServerFailed = Get-WsusServer failed to return a WSUS Server. The server may not yet have been configured. +WSUSConfigurationFailed = WSUS Computer Target Group configuration failed. +GetWsusServerSucceeded = WSUS Server information has been successfully retrieved from server '{0}'. +NotFoundComputerTargetGroup = A Computer Target Group with Name '{0}' was not found at Path '{1}'. +DuplicateComputerTargetGroup = A Computer Target Group with Name '{0}' already exists at Path '{1}'. +FoundComputerTargetGroup = Successfully located Computer Target Group with Name '{0}' at Path '{1}' with ID '{2}'. +ResourceInDesiredState = The Computer Target Group '{0}' at Path '{1}' is '{2}' which is the desired state. +ResourceNotInDesiredState = The Computer Target Group '{0}' at Path '{1}' is '{2}' which is NOT the desired state. +FoundParentComputerTargetGroup = Successfully located Parent Computer Target Group with Name '{0}' at Path '{1}' with ID '{2}'. +NotFoundParentComputerTargetGroup = The Parent Computer Target Group with Name '{0}' was not found at Path '{1}'. The new Computer Target Group '{2}' cannot be created. +CreateComputerTargetGroupFailed = An error occurred creating the Computer TargetGroup '{0}' at Path '{1}'. +CreateComputerTargetGroupSuccess = The Computer Target Group '{0}' was successfully created at Path '{1}'. +DeleteComputerTargetGroupFailed = An error occurred deleting the Computer TargetGroup '{0}' with ID '{1}' from Path '{2}'. +DeleteComputerTargetGroupSuccess = The Computer Target Group '{0}' with ID '{1}' was successfully deleted from Path '{2}'. +'@ diff --git a/source/Examples/Resources/UpdateServicesComputerTargetGroup/1-UpdateServicesComputerTargetGroup_AddComputerTargetGroup_Config.ps1 b/source/Examples/Resources/UpdateServicesComputerTargetGroup/1-UpdateServicesComputerTargetGroup_AddComputerTargetGroup_Config.ps1 new file mode 100644 index 0000000..87aace0 --- /dev/null +++ b/source/Examples/Resources/UpdateServicesComputerTargetGroup/1-UpdateServicesComputerTargetGroup_AddComputerTargetGroup_Config.ps1 @@ -0,0 +1,47 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 07ae5437-126c-480f-a9ab-af3241614c82 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/UpdateServicesDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/UpdateServicesDsc +.ICONURI https://dsccommunity.org/images/DSC_Logo_300p.png +.RELEASENOTES +Updated author, copyright notice, and URLs. +#> + +#Requires -Module UpdateServicesDsc + +<# + .DESCRIPTION + This configuration will create two WSUS Computer Target Groups + (a Parent and a Child) +#> +Configuration UpdateServicesComputerTargetGroup_AddComputerTargetGroup_Config +{ + param + ( + ) + + Import-DscResource -ModuleName UpdateServicesDsc + + node localhost + { + UpdateServicesComputerTargetGroup 'ComputerTargetGroup_Servers' + { + Name = 'Servers' + Path = 'All Computers' + Ensure = 'Present' + } + + UpdateServicesComputerTargetGroup 'ComputerTargetGroup_Web' + { + Name = 'Web' + Path = 'All Computers/Servers' + Ensure = 'Present' + DependsOn = '[UpdateServicesComputerTargetGroup]ComputerTargetGroup_Servers' + } + } +} diff --git a/source/Examples/Resources/UpdateServicesComputerTargetGroup/2-UpdateServicesComputerTargetGroup_DeleteComputerTargetGroup_Config.ps1 b/source/Examples/Resources/UpdateServicesComputerTargetGroup/2-UpdateServicesComputerTargetGroup_DeleteComputerTargetGroup_Config.ps1 new file mode 100644 index 0000000..6d555ff --- /dev/null +++ b/source/Examples/Resources/UpdateServicesComputerTargetGroup/2-UpdateServicesComputerTargetGroup_DeleteComputerTargetGroup_Config.ps1 @@ -0,0 +1,38 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 9165945f-cd15-4865-aa3a-1ac6b6f94a8b +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/UpdateServicesDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/UpdateServicesDsc +.ICONURI https://dsccommunity.org/images/DSC_Logo_300p.png +.RELEASENOTES +Updated author, copyright notice, and URLs. +#> + +#Requires -Module UpdateServicesDsc + +<# + .DESCRIPTION + This configuration will delete a WSUS Computer Target Group +#> +Configuration UpdateServicesComputerTargetGroup_DeleteComputerTargetGroup_Config +{ + param + ( + ) + + Import-DscResource -ModuleName UpdateServicesDsc + + node localhost + { + UpdateServicesComputerTargetGroup 'ComputerTargetGroup_Web' + { + Name = 'Web' + Path = 'All Computers/Servers' + Ensure = 'Absent' + } + } +} diff --git a/source/UpdateServicesDsc.psd1 b/source/UpdateServicesDsc.psd1 index 67e186d..3e71343 100644 --- a/source/UpdateServicesDsc.psd1 +++ b/source/UpdateServicesDsc.psd1 @@ -73,7 +73,7 @@ AliasesToExport = @() # DSC resources to export from this module - DscResourcesToExport = @('UpdateServicesApprovalRule', 'UpdateServicesCleanup','UpdateServicesServer') + DscResourcesToExport = @('UpdateServicesApprovalRule', 'UpdateServicesCleanup','UpdateServicesComputerTargetGroup','UpdateServicesServer') # List of all modules packaged with this module # ModuleList = @()