diff --git a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 index a4beb4c9b..7650039c5 100644 --- a/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 +++ b/source/DSCResources/DSC_xServiceResource/DSC_xServiceResource.psm1 @@ -628,12 +628,10 @@ function Test-TargetResource New-InvalidArgumentException -ArgumentName 'BuiltInAccount / Credential / GroupManagedServiceAccount' -Message $errorMessage } - if(($PSBoundParameters.ContainsKey('Failure3Action') -and (-not $PSBoundParameters.ContainsKey('Failure2Action'))) -or - ($PSBoundParameters.ContainsKey('Failure2Action') -and (-not $PSBoundParameters.ContainsKey('Failure1Action'))) - ) + if ($PSBoundParameters.ContainsKey('FailureCommand') -and (-not (Test-HasRestartFailureAction -Collection $FailureActionsCollection))) { - $errorMessage = $script:localizedData.FailureActionsMustBeSpecifiedInOrder - New-InvalidArgumentException -ArgumentName 'Failure2Action / Failure3Action' -Message $errorMessage + $errorMessage = $script:localizedData.MustSpecifyRestartFailureAction + New-InvalidArgumentException -ArgumentName 'FailureCommand' -Message $errorMessage } $serviceResource = Get-TargetResource -Name $Name @@ -2458,3 +2456,25 @@ function Set-ServiceFailureActionProperty { } } } + +function Test-HasRestartFailureAction + { + [CmdletBinding()] + param ( + [Parameter()] + [System.Object[]] + $Collection + ) + + process { + $hasRestartAction = $false + + foreach ($action in $collection) { + if ($action.type -eq 'RUN_COMMAND') { + $hasRestartAction = $true + } + } + + $hasRestartAction + } + } diff --git a/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 b/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 index c3ca06e89..377ff19da 100644 --- a/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 +++ b/source/DSCResources/DSC_xServiceResource/en-US/DSC_xServiceResource.strings.psd1 @@ -37,5 +37,5 @@ ConvertFrom-StringData @' CannotGetAccountAccessErrorMessage = Failed to get user policy rights. CannotSetAccountAccessErrorMessage = Failed to set user policy rights. CorruptDependency = Service '{0}' has a corrupt dependency. For more information, inspect the registry value at HKLM:\\SYSTEM\\CurrentControlSet\\Services\\{0}\\DependOnService. - FailureActionsMustBeSpecifiedInOrder = Failure actions must be specified in order from 1 to 3. + MustSpecifyRestartFailureAction = A failure command can only be specified if one of the failure actions is 'RUN_COMMAND' '@ diff --git a/tests/Unit/DSC_xServiceResource.Tests.ps1 b/tests/Unit/DSC_xServiceResource.Tests.ps1 index f55f5adb1..9f1c2bf4a 100644 --- a/tests/Unit/DSC_xServiceResource.Tests.ps1 +++ b/tests/Unit/DSC_xServiceResource.Tests.ps1 @@ -275,13 +275,34 @@ try failureActionCount = 1 failureCommand = $null rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ + actionsCollection = @(@{ type = 'RESTART' delaySeconds = 30000 }) failureActionsOnNonCrashFailures = $true } } + '3ActionsCommandAndMessage' { + $data = @{ + resetPeriodSeconds = 172800 + hasRebootMessage = 1 + hasFailureCommand = 1 + failureActionCount = 3 + failureCommand = 'C:\new\command.exe' + rebootMessage = 'Mocked Reboot Message' + actionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 30000 + },@{ + type = 'RUN_COMMAND' + delaySeconds = 30000 + },@{ + type = 'REBOOT' + delaySeconds = 30000 + }) + failureActionsOnNonCrashFailures = $true + } + } Default {} } @@ -423,24 +444,12 @@ try DesktopInteract = $false } - $testServiceFailureActions = @{ - resetPeriodSeconds = 86400 - hasRebootMessage = 0 - hasFailureCommand = 0 - failureActionCount = 1 - failureCommand = $null - rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ - type = 'RESTART' - delaySeconds = 30000 - }) - failureActionsOnNonCrashFailures = $true - } + $testServiceFailureActions, $failureRegistryKey = Get-MockFailureActionsData -DataSetName '3ActionsCommandAndMessage' Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } - Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } + Mock -CommandName 'Get-Item' -ParameterFilter { $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$($testService.name)" } -MockWith { return $failureRegistryKey} Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance @@ -492,24 +501,12 @@ try DesktopInteract = $false } - $testServiceFailureActions = @{ - resetPeriodSeconds = 86400 - hasRebootMessage = 0 - hasFailureCommand = 0 - failureActionCount = 1 - failureCommand = $null - rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ - type = 'RESTART' - delaySeconds = 30000 - }) - failureActionsOnNonCrashFailures = $true - } + $testServiceFailureActions, $failureRegistryKey = Get-MockFailureActionsData -DataSetName '1ActionNoCommandOrMessage' Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } - Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } + Mock -CommandName 'Get-Item' -ParameterFilter { $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$($testService.name)" } -MockWith { return $failureRegistryKey} Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance @@ -598,25 +595,13 @@ try DesktopInteract = $false } - $testServiceFailureActions = @{ - resetPeriodSeconds = 86400 - hasRebootMessage = 0 - hasFailureCommand = 0 - failureActionCount = 1 - failureCommand = $null - rebootMessage = $null - actionsCollection = @([PSCustomObject]@{ - type = 'RESTART' - delaySeconds = 30000 - }) - failureActionsOnNonCrashFailures = $true - } + $testServiceFailureActions, $failureRegistryKey = Get-MockFailureActionsData -DataSetName '1ActionNoCommandOrMessage' Mock -CommandName 'Get-Service' -MockWith { return $testService } Mock -CommandName 'Get-ServiceCimInstance' -MockWith { return $testServiceCimInstance } Mock -CommandName 'ConvertTo-StartupTypeString' -MockWith { return $convertToStartupTypeStringResult } Mock -CommandName 'Write-Warning' - Mock -CommandName 'Get-ServiceFailureActions' -MockWith { return $testServiceFailureActions } + Mock -CommandName 'Get-Item' -ParameterFilter { $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$($testService.name)" } -MockWith { return $failureRegistryKey} Test-GetTargetResourceDoesntThrow -GetTargetResourceParameters $getTargetResourceParameters -TestServiceCimInstance $testServiceCimInstance @@ -805,6 +790,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -832,7 +825,20 @@ try } It 'Should change all service properties except Credential' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) -and + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should start the service' { @@ -856,6 +862,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -883,7 +897,20 @@ try } It 'Should change all service properties except BuiltInAccount' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Credential -DifferenceObject $Credential) -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Credential -DifferenceObject $Credential) -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -934,7 +961,9 @@ try } It 'Should set the service to start with the GroupManagedServiceAccount' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $GroupManagedServiceAccount -eq $setTargetResourceParameters.GroupManagedServiceAccount } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and$StartupType -eq $setTargetResourceParameters.StartupType -and$GroupManagedServiceAccount -eq $setTargetResourceParameters.GroupManagedServiceAccount + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -1105,6 +1134,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -1132,7 +1169,20 @@ try } It 'Should change all service properties except Credential' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { + $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -1155,6 +1205,14 @@ try DisplayName = 'TestDisplayName' Description = 'Test device description' Dependencies = @( 'TestServiceDependency1', 'TestServiceDependency2' ) + ResetPeriodSeconds = 86400 + RebootMessage = 'RebootMessage' + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @(@{ + type = 'RESTART' + delaySeconds = 500 + }) + FailureActionsOnNonCrashFailures = $true } It 'Should not throw' { @@ -1182,7 +1240,19 @@ try } It 'Should change all service properties except BuiltInAccount' { - Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and $StartupType -eq $setTargetResourceParameters.StartupType -and $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and $DisplayName -eq $setTargetResourceParameters.DisplayName -and $Description -eq $setTargetResourceParameters.Description -and $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) } -Times 1 -Scope 'Context' + Assert-MockCalled -CommandName 'Set-ServiceProperty' -ParameterFilter { $ServiceName -eq $setTargetResourceParameters.Name -and + $StartupType -eq $setTargetResourceParameters.StartupType -and + $BuiltInAccount -eq $setTargetResourceParameters.BuiltInAccount -and + $DesktopInteract -eq $setTargetResourceParameters.DesktopInteract -and + $DisplayName -eq $setTargetResourceParameters.DisplayName -and + $Description -eq $setTargetResourceParameters.Description -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Dependencies -DifferenceObject $Dependencies) + $ResetPeriodSeconds -eq $setTargetResourceParameters.resetPeriodSeconds -and + $RebootMessage -eq $setTargetResourceParameters.rebootMessage -and + $FailureCommand -eq $setTargetResourceParameters.failureCommand -and + $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.failureActionsCollection -DifferenceObject $FailureActionsCollection) -and + $FailureActionsOnNonCrashFailures -eq $setTargetResourceParameters.failureActionsOnNonCrashFailures + } -Times 1 -Scope 'Context' } It 'Should not attempt to start the service' { @@ -1329,6 +1399,17 @@ try $expectedErrorMessage = $script:localizedData.CredentialParametersAreMutallyExclusive -f $testTargetResourceParameters.Name { Test-TargetResource @testTargetResourceParameters } | Should -Throw -ExpectedMessage $expectedErrorMessage } + + $testTargetResourceParameters = @{ + Name = $script:testServiceName + FailureCommand = 'C:\Path\To\Command.exe' + FailureActionsCollection = @{Type = 'RESTART'; Delay = 180} + } + + It 'Should throw an error for invalid use of FailureCommand parameter' { + $expectedErrorMessage = $script:localizedData.MustSpecifyRestartFailureAction + { Test-TargetResource @testTargetResourceParameters } | Should -Throw -ExpectedMessage $expectedErrorMessage + } } Context 'When a service does not exist and Ensure set to Absent' {