From 5241cc44c718a1f0eb61e42eb2e94e9338da8da2 Mon Sep 17 00:00:00 2001 From: Daniel Krebs Date: Sun, 4 Sep 2016 12:03:14 +0700 Subject: [PATCH 1/2] Fix #132: PSSA and style issues --- .../MSFT_xProcessResource.psm1 | 307 +++++++++++++----- .../MSFT_xWindowsOptionalFeature.psm1 | 187 ++++++++--- Examples/Sample_xWindowsOptionalFeature.ps1 | 22 ++ README.md | 8 + .../MSFT_xWindowsOptionalFeature.Tests.ps1 | 108 ++++++ .../MSFT_xWindowsOptionalFeature.Tests.ps1 | 102 +++--- 6 files changed, 555 insertions(+), 179 deletions(-) create mode 100644 Examples/Sample_xWindowsOptionalFeature.ps1 create mode 100644 Tests/Integration/MSFT_xWindowsOptionalFeature.Tests.ps1 diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 15fc1cbcf..c7fba4f51 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -24,6 +24,12 @@ ErrorParametersNotSupportedWithCredential = Can't specify StandardOutputPath, St VerboseInProcessHandle = In process handle {0} ErrorRunAsCredentialParameterNotSupported = The PsDscRunAsCredential parameter is not supported by the Process resource. To start the process with user '{0}', add the Credential parameter. ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredential parameter is not supported by the Process resource, and cannot be used with the Credential parameter. To start the process with user '{0}', use only the Credential parameter, not the PsDscRunAsCredential parameter. +GetTargetResourceStartMessage = Begin executing Get functionality for the process {0}. +GetTargetResourceEndMessage = End executing Get functionality for the process {0}. +SetTargetResourceStartMessage = Begin executing Set functionality for the process {0}. +SetTargetResourceEndMessage = End executing Set functionality for the process {0}. +TestTargetResourceStartMessage = Begin executing Test functionality for the process {0}. +TestTargetResourceEndMessage = End executing Test functionality for the process {0}. '@ } @@ -42,10 +48,10 @@ function Test-IsRunFromLocalSystemUser [CmdletBinding()] param () + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList $identity - $currentUser = (New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList @( [Security.Principal.WindowsIdentity]::GetCurrent() )) - - return $currenUser.Identity.IsSystem + return $principal.Identity.IsSystem } <# @@ -68,27 +74,28 @@ function Split-Credential [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [PSCredential] + [System.Management.Automation.Credential()] $Credential ) $wrongFormat = $false - if ($Credential.UserName.Contains('\')) + if ($Credential.UserName.Contains('\')) { $credentialSegments = $Credential.UserName.Split('\') - + if ($credentialSegments.Length -gt 2) { # i.e. domain\user\foo $wrongFormat = $true - } + } else { $domain = $credentialSegments[0] $userName = $credentialSegments[1] } - } - elseif ($Credential.UserName.Contains('@')) + } + elseif ($Credential.UserName.Contains('@')) { $credentialSegments = $Credential.UserName.Split('@') @@ -103,17 +110,17 @@ function Split-Credential $Domain = $credentialSegments[1] } } - else + else { # support for default domain (localhost) $domain = $env:computerName $userName = $Credential.UserName } - if ($wrongFormat) + if ($wrongFormat) { $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName - + Write-Verbose -Message $message New-InvalidArgumentException -ArgumentName 'Credential' -Message $message @@ -125,6 +132,24 @@ function Split-Credential } } +<# + .SYNOPSIS + Gets the state of the managed process. + + .PARAMETER Path + The path to the process executable. If this the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will not use the Path environment variable to find the file, and will throw an + error if the path does not exist. Relative paths are not allowed. + + .PARAMETER Arguments + Indicates a string of arguments to pass to the process as-is. If you need to pass several + arguments, put them all in this string. + + .PARAMETER Credential + Indicates the credentials for starting the process. +#> function Get-TargetResource { [OutputType([Hashtable])] @@ -143,9 +168,12 @@ function Get-TargetResource [ValidateNotNullOrEmpty()] [PSCredential] + [System.Management.Automation.Credential()] $Credential ) - + + Write-Verbose ($LocalizedData.GetTargetResourceStartMessage -f $Path) + $Path = Expand-Path -Path $Path $getWin32ProcessArguments = @{ @@ -184,8 +212,46 @@ function Get-TargetResource ProcessId = $win32Process.ProcessId } } + + Write-Verbose ($LocalizedData.GetTargetResourceEndMessage -f $Path) } +<# + .SYNOPSIS + Ensures the managed process executable is Present or Absent. + + .PARAMETER Path + The path to the process executable. If this the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will not use the Path environment variable to find the file, and will throw an + error if the path does not exist. Relative paths are not allowed. + + .PARAMETER Arguments + Indicates a string of arguments to pass to the process as-is. If you need to pass several + arguments, put them all in this string. + + .PARAMETER Credential + Indicates the credentials for starting the process. + + .PARAMETER Ensure + Indicates if the process exists. Set this property to "Present" to ensure that the process + exists. Otherwise, set it to "Absent". The default is "Present". + + .PARAMETER StandardOutputPath + Indicates the location to write the standard output. Any existing file there will be + overwritten. + + .PARAMETER StandardErrorPath + Indicates the directory path to write the standard error. Any existing file there will be + overwritten. + + .PARAMETER StandardInputPath + Indicates the standard input location. + + .PARAMETER WorkingDirectory + Indicates the location that will be used as the current working directory for the process. +#> function Set-TargetResource { [CmdletBinding(SupportsShouldProcess = $true)] @@ -203,6 +269,7 @@ function Set-TargetResource [ValidateNotNullOrEmpty()] [PSCredential] + [System.Management.Automation.Credential()] $Credential, [ValidateSet('Present', 'Absent')] @@ -222,10 +289,17 @@ function Set-TargetResource $WorkingDirectory ) + Write-Verbose ($LocalizedData.SetTargetResourceStartMessage -f $Path) + if ($null -ne $PsDscContext.RunAsUser) { - New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) - } + $params = @{ + ArgumentName = 'PsDscRunAsCredential' + Message = + ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + } + New-InvalidArgumentException @params + } $Path = Expand-Path -Path $Path @@ -243,29 +317,40 @@ function Set-TargetResource if ($Ensure -eq 'Absent') { - Assert-HashtableDoesNotContainKey -Hashtable $PSBoundParameters -Key @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) + $params = @{ + Hashtable = $PSBoundParameters + Key = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', + 'WorkingDirectory' ) + } + Assert-HashtableDoesNotContainKey @params - if ($win32Processes.Count -gt 0 -and $PSCmdlet.ShouldProcess($Path, $LocalizedData.StoppingProcessWhatif)) + $whatIfShouldProcess = $PSCmdlet.ShouldProcess($Path, $LocalizedData.StoppingProcessWhatif) + if ($win32Processes.Count -gt 0 -and $whatIfShouldProcess) { - $processIds = $win32Processes.ProcessId + $processIds = $win32Processes.ProcessId - $stopProcessError = Stop-Process -Id $processIds -Force 2>&1 - - if ($null -eq $stopProcessError) - { - Write-Verbose -Message ($LocalizedData.ProcessesStopped -f $Path, ($processIds -join ',')) - } - else - { - Write-Verbose -Message ($LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), ($stopProcessError | Out-String)) - throw $stopProcessError + $stopProcessError = Stop-Process -Id $processIds -Force 2>&1 + + if ($null -eq $stopProcessError) + { + $message = ($LocalizedData.ProcessesStopped -f $Path, ($processIds -join ',')) + Write-Verbose -Message $message + } + else + { + $message = ($LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), + ($stopProcessError | Out-String)) + Write-Verbose -Message $message + throw $stopProcessError } - # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work + # Before returning from Set-TargetResource we have to ensure a subsequent + # Test-TargetResource is going to work if (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 0)) { - $message = $LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), $LocalizedData.FailureWaitingForProcessesToStop - + $message = $LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), + $LocalizedData.FailureWaitingForProcessesToStop + Write-Verbose -Message $message New-InvalidOperationException -Message $message @@ -278,13 +363,18 @@ function Set-TargetResource } else { - $shouldBeRootedPathArguments = @( 'StandardInputPath', 'WorkingDirectory', 'StandardOutputPath', 'StandardErrorPath' ) + $shouldBeRootedPathArguments = @( 'StandardInputPath', 'WorkingDirectory', + 'StandardOutputPath', 'StandardErrorPath' ) foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments) { if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument])) { - Assert-PathArgumentRooted -PathArgumentName $shouldBeRootedPathArgument -PathArgument $PSBoundParameters[$shouldBeRootedPathArgument] + $params = @{ + PathArgumentName = $shouldBeRootedPathArgument + PathArgument = $PSBoundParameters[$shouldBeRootedPathArgument] + } + Assert-PathArgumentRooted @params } } @@ -294,7 +384,11 @@ function Set-TargetResource { if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument])) { - Assert-PathArgumentExists -PathArgumentName $shouldExistPathArgument -PathArgument $PSBoundParameters[$shouldExistPathArgument] + $params = @{ + PathArgumentName = $shouldExistPathArgument + PathArgument = $PSBoundParameters[$shouldExistPathArgument] + } + Assert-PathArgumentValid @params } } @@ -314,9 +408,11 @@ function Set-TargetResource foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) { - if (-not [String]::IsNullOrEmpty($PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]])) + $parameterKey = $startProcessOptionalArgumentMap[$startProcessOptionalArgumentName] + $value = $PSBoundParameters[$parameterKey] + if (-not [String]::IsNullOrEmpty($value)) { - $startProcessArguments[$startProcessOptionalArgumentName] = $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]] + $startProcessArguments[$startProcessOptionalArgumentName] = $value } } @@ -329,34 +425,32 @@ function Set-TargetResource { <# Start-Process calls .net Process.Start() - If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx + If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW + http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx CreateProcessWithLogonW cannot be called as LocalSystem user. - Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx (section Remarks/Windows XP with SP2 and Windows Server 2003) - + Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx + (section Remarks/Windows XP with SP2 and Windows Server 2003) + In this case we call another api. #> if ($PSBoundParameters.ContainsKey('Credential') -and (Test-IsRunFromLocalSystemUser)) { - if ($PSBoundParameters.ContainsKey('StandardOutputPath')) - { - New-InvalidArgumentException -ArgumentName 'StandardOutputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential - } - - if ($PSBoundParameters.ContainsKey('StandardInputPath')) + foreach ($key in @('StandardOutputPath','StandardInputPath','WorkingDirectory')) { - New-InvalidArgumentException -ArgumentName 'StandardInputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential - } - - if ($PSBoundParameters.ContainsKey('WorkingDirectory')) - { - New-InvalidArgumentException -ArgumentName 'WorkingDirectory' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + $params = { + ArgumentName = $key + Message = $LocalizedData.ErrorParametersNotSupportedWithCredential + } + New-InvalidArgumentException @params } $splitCredentialResult = Split-Credential $Credential try { <# - Internally we use win32 api LogonUser() with dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. + Internally we use win32 api LogonUser() with + dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. + It grants process ability for second-hop. #> Import-DscNativeMethods @@ -379,17 +473,19 @@ function Set-TargetResource } else { - Write-Verbose -Message ($LocalizedData.ErrorStarting -f $Path, ($startProcessError | Out-String)) + Write-Verbose -Message ($LocalizedData.ErrorStarting -f $Path, + ($startProcessError | Out-String)) throw $startProcessError } # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work if (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 1)) { - $message = $LocalizedData.ErrorStarting -f $Path, $LocalizedData.FailureWaitingForProcessesToStart - + $message = $LocalizedData.ErrorStarting -f $Path, + $LocalizedData.FailureWaitingForProcessesToStart + Write-Verbose -Message $message - + New-InvalidOperationException -Message $message } } @@ -399,8 +495,46 @@ function Set-TargetResource Write-Verbose -Message ($LocalizedData.ProcessAlreadyStarted -f $Path) } } + + Write-Verbose ($LocalizedData.SetTargetResourceEndMessage -f $Path) } +<# + .SYNOPSIS + Tests if the managed process is Present or Absent. + + .PARAMETER Path + The path to the process executable. If this the file name of the executable + (not the fully qualified path), the DSC resource will search the environment Path variable + ($env:Path) to find the executable file. If the value of this property is a fully qualified + path, DSC will not use the Path environment variable to find the file, and will throw an + error if the path does not exist. Relative paths are not allowed. + + .PARAMETER Arguments + Indicates a string of arguments to pass to the process as-is. If you need to pass several + arguments, put them all in this string. + + .PARAMETER Credential + Indicates the credentials for starting the process. + + .PARAMETER Ensure + Indicates if the process exists. Set this property to "Present" to ensure that the process + exists. Otherwise, set it to "Absent". The default is "Present". + + .PARAMETER StandardOutputPath + Indicates the location to write the standard output. Any existing file there will be + overwritten. + + .PARAMETER StandardErrorPath + Indicates the directory path to write the standard error. Any existing file there will be + overwritten. + + .PARAMETER StandardInputPath + Indicates the standard input location. + + .PARAMETER WorkingDirectory + Indicates the location that will be used as the current working directory for the process. +#> function Test-TargetResource { [OutputType([Boolean])] @@ -419,6 +553,7 @@ function Test-TargetResource [ValidateNotNullOrEmpty()] [PSCredential] + [System.Management.Automation.Credential()] $Credential, [ValidateSet('Present', 'Absent')] @@ -438,10 +573,17 @@ function Test-TargetResource $WorkingDirectory ) + Write-Verbose ($LocalizedData.TestTargetResourceStartMessage -f $Path) + if ($null -ne $PsDscContext.RunAsUser) { - New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) - } + $params = @{ + ArgumentName = 'PsDscRunAsCredential' + Message = + ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + } + New-InvalidArgumentException @params + } $Path = Expand-Path -Path $Path @@ -457,6 +599,7 @@ function Test-TargetResource $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) + Write-Verbose ($LocalizedData.TestTargetResourceEndMessage -f $Path) if ($Ensure -eq 'Absent') { return ($win32Processes.Count -eq 0) @@ -490,12 +633,12 @@ function Get-Win32ProcessOwner ) $owner = Invoke-CimMethod -InputObject $Process -MethodName 'GetOwner' -ErrorAction 'SilentlyContinue' - + if ($null -ne $owner.Domain) { return $owner.Domain + '\' + $owner.User } - else + else { return $owner.User } @@ -534,7 +677,7 @@ function Wait-ProcessCount { $actualProcessCount = @( Get-Win32Process @ProcessSettings ).Count } while ($actualProcessCount -ne $ProcessCount -and ([DateTime]::Now - $startTime).TotalMilliseconds -lt 2000) - + return $actualProcessCount -eq $ProcessCount } @@ -560,7 +703,7 @@ function Wait-ProcessCount function Get-Win32Process { [OutputType([Object[]])] - [CmdletBinding(SupportsShouldProcess = $true)] + [CmdletBinding()] param ( [Parameter(Mandatory = $true)] @@ -573,6 +716,7 @@ function Get-Win32Process [ValidateNotNullOrEmpty()] [PSCredential] + [System.Management.Automation.Credential()] $Credential, [ValidateRange(0, [Int]::MaxValue)] @@ -588,7 +732,7 @@ function Get-Win32Process if ($getProcessResult.Count -ge $UseGetCimInstanceThreshold) { - + $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path $wqlFilter = "ExecutablePath = '$escapedPathForWqlFilter'" @@ -601,7 +745,12 @@ function Get-Win32Process if ($process.Path -ieq $Path) { Write-Verbose -Message ($LocalizedData.VerboseInProcessHandle -f $process.Id) - $processes += Get-CimInstance -ClassName 'Win32_Process' -Filter "ProcessId = $($process.Id)" -ErrorAction 'SilentlyContinue' + $params = @{ + ClassName = 'Win32_Process' + Filter = "ProcessId = $($process.Id)" + ErrorAction = 'SilentlyContinue' + } + $processes += Get-CimInstance @params } } } @@ -610,7 +759,13 @@ function Get-Win32Process { $splitCredentialResult = Split-Credenital -Credential $Credential - $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$($splitCredentialResult.Domain)\$($splitCredentialResult.UserName)" } + $whereFilterScript = { + $domain = $splitCredentialResult.Domain + $userName = $splitCredentialResult.UserName + + (Get-Win32ProcessOwner -Process $_) -eq "${domain}\${userName}" + } + $processes = Where-Object -InputObject $processes -FilterScript $whereFilterScript } if ($null -eq $Arguments) @@ -622,7 +777,8 @@ function Get-Win32Process foreach ($process in $processes) { - if ((Get-ArgumentsFromCommandLineInput -CommandLineInput ($process.CommandLine)) -eq $Arguments) + $commandLine = $process.CommandLine + if ((Get-ArgumentsFromCommandLineInput -CommandLineInput $commandLine) -eq $Arguments) { $processesWithMatchingArguments += $process } @@ -656,7 +812,7 @@ function Get-ArgumentsFromCommandLineInput { return [String]::Empty } - + $CommandLineInput = $CommandLineInput.Trim() if ($CommandLineInput.StartsWith('"')) @@ -753,9 +909,6 @@ function Expand-Path { $envPathSegment = [Environment]::ExpandEnvironmentVariables($rawEnvPathSegment) - # If an exception causes $envPathSegmentRooted not to be set, we will consider it $false - $envPathSegmentRooted = $false - <# If the whole path passed through [IO.Path]::IsPathRooted with no exceptions, it does not have invalid characters, so the segment has no invalid characters and will not throw as well. @@ -764,12 +917,16 @@ function Expand-Path { $envPathSegmentRooted = [IO.Path]::IsPathRooted($envPathSegment) } - catch {} - + catch + { + # If an exception causes $envPathSegmentRooted not to be set, we will consider it $false + $envPathSegmentRooted = $false + } + if ($envPathSegmentRooted) { $fullPathCandidate = Join-Path -Path $envPathSegment -ChildPath $Path - + if (Test-Path -Path $fullPathCandidate -PathType 'Leaf') { return $fullPathCandidate @@ -805,7 +962,7 @@ function Assert-PathArgumentRooted [String] $PathArgument ) - + if (-not ([IO.Path]::IsPathRooted($PathArgument))) { New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldBeAbsolute) @@ -822,7 +979,7 @@ function Assert-PathArgumentRooted .PARAMETER PathArgument The path argument that should exist. #> -function Assert-PathArgumentExists +function Assert-PathArgumentValid { [CmdletBinding()] param @@ -869,7 +1026,7 @@ function Assert-HashtableDoesNotContainKey foreach ($keyName in $Key) { - if ($Hashtable.ContainsKey($keyName)) + if ($Hashtable.ContainsKey($keyName)) { New-InvalidArgumentException -ArgumentName $keyName -Message ($LocalizedData.ParameterShouldNotBeSpecified -f $keyName) } diff --git a/DSCResources/MSFT_xWindowsOptionalFeature/MSFT_xWindowsOptionalFeature.psm1 b/DSCResources/MSFT_xWindowsOptionalFeature/MSFT_xWindowsOptionalFeature.psm1 index 87485992d..0d656d46d 100644 --- a/DSCResources/MSFT_xWindowsOptionalFeature/MSFT_xWindowsOptionalFeature.psm1 +++ b/DSCResources/MSFT_xWindowsOptionalFeature/MSFT_xWindowsOptionalFeature.psm1 @@ -1,4 +1,10 @@ -# This PS module contains functions for Desired State Configuration Windows Optional Feature provider. It enables configuring optional features on Windows Client SKUs. +# This module contains functions for Desired State Configuration Windows Optional Feature provider. +# It enables configuring optional features on Windows Client SKUs. + +# Suppress PSSA issue PSAvoidGlobalVars because setting $global:DSCMachineStatus must be used +# for this resource to notify the LCM about a required restart to complete the action. +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] +param () # Fallback message strings in en-US DATA localizedData @@ -20,10 +26,19 @@ DATA localizedData TestTargetResourceEndMessage = End executing Test functionality on the {0} feature. FeatureInstalled = Installed feature {0}. FeatureUninstalled = Uninstalled feature {0}. + EnableFeature = Enable a Windows optional feature + DisableFeature = Disable a Windows optional feature '@ } Import-Module Dism -Force -ErrorAction SilentlyContinue +<# + .SYNOPSIS + Gets the state of a Windows optional feature + + .PARAMETER Name + Specify the name of the Windows optional feature +#> function Get-TargetResource { [CmdletBinding()] @@ -35,16 +50,17 @@ function Get-TargetResource $Name ) - Write-Debug ($LocalizedData.GetTargetResourceStartMessage -f $Name) + Write-Verbose ($LocalizedData.GetTargetResourceStartMessage -f $Name) - ValidatePrerequisites + Assert-ResourcePrerequisitesValid $result = Dism\Get-WindowsOptionalFeature -FeatureName $Name -Online $returnValue = @{ LogPath = $result.LogPath - Ensure = ConvertStateToEnsure $result.State - CustomProperties = SerializeCustomProperties $result.CustomProperties + Ensure = Convert-FeatureStateToEnsure $result.State + CustomProperties = + Get-SerializedCustomPropertyList -CustomProperties $result.CustomProperties Name = $result.FeatureName LogLevel = $result.LogLevel Description = $result.Description @@ -53,22 +69,35 @@ function Get-TargetResource $returnValue - Write-Debug ($LocalizedData.GetTargetResourceEndMessage -f $Name) + Write-Verbose ($LocalizedData.GetTargetResourceEndMessage -f $Name) } -# Serializes a list of CustomProperty objects into [System.String[]] -function SerializeCustomProperties +<# + .SYNOPSIS + Serializes a list of CustomProperty objects serialized into [System.String[]] + + .PARAMETER CustomProperties + Provide a list of CustomProperty objects to be serialized +#> +function Get-SerializedCustomPropertyList { param ( $CustomProperties ) - $CustomProperties | ? {$_ -ne $null} | % { "Name = $($_.Name), Value = $($_.Value), Path = $($_.Path)" } + $CustomProperties | Where-Object { $_ -ne $null } | + ForEach-Object { "Name = $($_.Name), Value = $($_.Value), Path = $($_.Path)" } } -# Converts state returned by Dism Get-WindowsOptionalFeature cmdlet to Present/Absent -function ConvertStateToEnsure +<# + .SYNOPSIS + Converts state returned by Dism Get-WindowsOptionalFeature cmdlet to Present/Absent + + .PARAMETER State + Provide a valid state Enabled or Disabled to be converted to either Present or Absent +#> +function Convert-FeatureStateToEnsure { param ( @@ -90,10 +119,39 @@ function ConvertStateToEnsure } } +<# + .SYNOPSIS + Enable or disable a Windows optional feature + + .PARAMETER Source + Not implemented. + + .PARAMETER RemoveFilesOnDisable + Set to $true to remove all files associated with the feature when it is disabled (that is, + when Ensure is set to "Absent"). + + .PARAMETER LogPath + The path to a log file where you want the resource provider to log the operation. + + .PARAMETER Ensure + Specifies whether the feature is enabled. To ensure that the feature is enabled, set this + property to "Present". To ensure that the feature is disabled, set the property to "Absent". + + .PARAMETER NoWindowsUpdateCheck + Specifies whether DISM contacts Windows Update (WU) when searching for the source files to + enable a feature. If $true, DISM does not contact WU. + .PARAMETER Name + Indicates the name of the feature that you want to ensure is enabled or disabled. + + .PARAMETER LogLevel + The maximum output level shown in the logs. The accepted values are: "ErrorsOnly" (only errors + are logged), "ErrorsAndWarning" (errors and warnings are logged), and + "ErrorsAndWarningAndInformation" (errors, warnings, and debug information are logged). +#> function Set-TargetResource { - [CmdletBinding()] + [CmdletBinding(SupportsShouldProcess=$true)] param ( [System.String[]] @@ -121,9 +179,9 @@ function Set-TargetResource $LogLevel ) - Write-Debug ($LocalizedData.SetTargetResourceStartMessage -f $Name) + Write-Verbose ($LocalizedData.SetTargetResourceStartMessage -f $Name) - ValidatePrerequisites + Assert-ResourcePrerequisitesValid switch ($LogLevel) { @@ -133,46 +191,42 @@ function Set-TargetResource '' { $DismLogLevel = 'WarningsInfo' } } - # construct parameters for Dism cmdlets - $PSBoundParameters.Remove('Name') > $null - $PSBoundParameters.Remove('Ensure') > $null - if ($PSBoundParameters.ContainsKey('RemoveFilesOnDisable')) - { - $PSBoundParameters.Remove('RemoveFilesOnDisable') - } - - if ($PSBoundParameters.ContainsKey('NoWindowsUpdateCheck')) - { - $PSBoundParameters.Remove('NoWindowsUpdateCheck') - } - - if ($PSBoundParameters.ContainsKey('LogLevel')) + # Construct splatting hashtable for Dism cmdlets + $cmdletParams = $PSBoundParameters.psobject.Copy() + $cmdletParams['FeatureName'] = $Name + $cmdletParams['Online'] = $true + $cmdletParams['LogLevel'] = $DismLogLevel + $cmdletParams['NoRestart'] = $true + foreach ($key in @('Name', 'Ensure','RemoveFilesOnDisable','NoWindowsUpdateCheck')) { - $PSBoundParameters.Remove('LogLevel') + if ($cmdletParams.ContainsKey($key)) + { + $cmdletParams.Remove($key) + } } if ($Ensure -eq 'Present') { - if ($NoWindowsUpdateCheck) + if ($PSCmdlet.ShouldProcess($Name, $LocalizedData.EnableFeature)) { - $feature = Dism\Enable-WindowsOptionalFeature -FeatureName $Name -Online -LogLevel $DismLogLevel @PSBoundParameters -LimitAccess -NoRestart - } - else - { - $feature = Dism\Enable-WindowsOptionalFeature -FeatureName $Name -Online -LogLevel $DismLogLevel @PSBoundParameters -NoRestart + if ($NoWindowsUpdateCheck) + { + $cmdletParams['LimitAccess'] = $true + } + $feature = Dism\Enable-WindowsOptionalFeature @cmdletParams } Write-Verbose ($LocalizedData.FeatureInstalled -f $Name) } elseif ($Ensure -eq 'Absent') { - if ($RemoveFilesOnDisable) + if ($PSCmdlet.ShouldProcess($Name, $LocalizedData.DisableFeature)) { - $feature = Dism\Disable-WindowsOptionalFeature -FeatureName $Name -Online -LogLevel $DismLogLevel @PSBoundParameters -Remove -NoRestart - } - else - { - $feature = Dism\Disable-WindowsOptionalFeature -FeatureName $Name -Online -LogLevel $DismLogLevel @PSBoundParameters -NoRestart + if ($RemoveFilesOnDisable) + { + $cmdletParams['Remove'] = $true + } + $feature = Dism\Disable-WindowsOptionalFeature @cmdletParams } Write-Verbose ($LocalizedData.FeatureUninstalled -f $Name) @@ -189,10 +243,39 @@ function Set-TargetResource $global:DSCMachineStatus = 1 } - Write-Debug ($LocalizedData.SetTargetResourceEndMessage -f $Name) + Write-Verbose ($LocalizedData.SetTargetResourceEndMessage -f $Name) } +<# + .SYNOPSIS + Test if a Windows optional feature is in the desired state (enabled or disabled) + + .PARAMETER Source + Not implemented. + + .PARAMETER RemoveFilesOnDisable + Set to $true to remove all files associated with the feature when it is disabled (that is, + when Ensure is set to "Absent"). + + .PARAMETER LogPath + The path to a log file where you want the resource provider to log the operation. + .PARAMETER Ensure + Specifies whether the feature is enabled. To ensure that the feature is enabled, set this + property to "Present". To ensure that the feature is disabled, set the property to "Absent". + + .PARAMETER NoWindowsUpdateCheck + Specifies whether DISM contacts Windows Update (WU) when searching for the source files to + enable a feature. If $true, DISM does not contact WU. + + .PARAMETER Name + Indicates the name of the feature that you want to ensure is enabled or disabled. + + .PARAMETER LogLevel + The maximum output level shown in the logs. The accepted values are: "ErrorsOnly" (only errors + are logged), "ErrorsAndWarning" (errors and warnings are logged), and + "ErrorsAndWarningAndInformation" (errors, warnings, and debug information are logged). +#> function Test-TargetResource { [CmdletBinding()] @@ -224,14 +307,14 @@ function Test-TargetResource $LogLevel ) - Write-Debug ($LocalizedData.TestTargetResourceStartMessage -f $Name) + Write-Verbose ($LocalizedData.TestTargetResourceStartMessage -f $Name) - ValidatePrerequisites + Assert-ResourcePrerequisitesValid $featureState = Dism\Get-WindowsOptionalFeature -FeatureName $Name -Online [bool] $result = $false - if ($featureState -eq $null) + if ($null -eq $featureState) { $result = $Ensure -eq 'Absent' } @@ -240,13 +323,15 @@ function Test-TargetResource { $result = $true } - Write-Debug ($LocalizedData.TestTargetResourceEndMessage -f $Name) + Write-Verbose ($LocalizedData.TestTargetResourceEndMessage -f $Name) return $result } - -# ValidatePrerequisites is a helper function used to validate if the MSFT_WindowsOptionalFeature is supported on the target machine. -function ValidatePrerequisites +<# + .SYNOPSIS + Helper function to test if the MSFT_WindowsOptionalFeature is supported on the target machine. +#> +function Assert-ResourcePrerequisitesValid { Write-Verbose $LocalizedData.ValidatingPrerequisites @@ -277,8 +362,4 @@ function ValidatePrerequisites } } - Export-ModuleMember -Function *-TargetResource - - - diff --git a/Examples/Sample_xWindowsOptionalFeature.ps1 b/Examples/Sample_xWindowsOptionalFeature.ps1 new file mode 100644 index 000000000..767940f13 --- /dev/null +++ b/Examples/Sample_xWindowsOptionalFeature.ps1 @@ -0,0 +1,22 @@ +# Installs the Windows optional feature 'TelnetClient' + +Configuration Sample_xWindowsOptionalFeature +{ + param + ( + [Parameter(Mandatory = $true)] + [String] + $LogPath + ) + + Import-DscResource -ModuleName xPSDesiredStateConfiguration + + xWindowsOptionalFeature TelnetClient + { + Name = 'TelnetClient' + Ensure = 'Present' + LogPath = $LogPath + } +} + +Sample_xWindowsOptionalFeature diff --git a/README.md b/README.md index 52a011516..87f282fdd 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,14 @@ These parameters will be the same for each Windows optional feature in the set. ### Unreleased +* xProcess: + * Fixed PSSA issues + * Corrected most style guideline issues +* xWindowsOptionalFeature: + * Cleaned up resource (PSSA issues, formatting, etc.) + * Added example script + * Added integration test + ### 3.13.0.0 * Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. diff --git a/Tests/Integration/MSFT_xWindowsOptionalFeature.Tests.ps1 b/Tests/Integration/MSFT_xWindowsOptionalFeature.Tests.ps1 new file mode 100644 index 000000000..29bb16490 --- /dev/null +++ b/Tests/Integration/MSFT_xWindowsOptionalFeature.Tests.ps1 @@ -0,0 +1,108 @@ +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xPSDesiredStateConfiguration' ` + -DSCResourceName 'MSFT_xWindowsOptionalFeature' ` + -TestType Integration + +Describe "xWindowsOptionalFeature Integration Tests" { + It "Install a valid Windows optional feature" { + $configurationName = "InstallOptionalFeature" + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + $logPath = Join-Path -Path (Get-Location) -ChildPath 'InstallOptionalFeatureLog' + + $validFeatureName = 'TelnetClient' + + $originalFeature = Dism\Get-WindowsOptionalFeature -Online -FeatureName $validFeatureName + + try + { + Configuration $configurationName + { + Import-DscResource -ModuleName xPSDesiredStateConfiguration + + xWindowsOptionalFeature WindowsOptionalFeature + { + Name = $validFeatureName + Ensure = "Present" + LogPath = $logPath + NoWindowsUpdateCheck = $true + } + } + + { & $configurationName -OutputPath $configurationPath } | Should Not Throw + + { Start-DscConfiguration -Path $configurationPath -Wait -Force -Verbose } | + Should Not Throw + + $windowsOptionalFeature = + Dism\Get-WindowsOptionalFeature -Online -FeatureName $validFeatureName + + $windowsOptionalFeature | Should Not Be $null + $windowsOptionalFeature.State -in 'Enabled','EnablePending' | Should Be $true + } + finally + { + if ($originalFeature.State -in 'Disabled','DisablePending') + { + Dism\Disable-WindowsOptionalFeature -Online -FeatureName $validFeatureName -NoRestart + } + + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + It "Install an incorrect Windows optional feature" { + $configurationName = "InstallIncorrectWindowsFeature" + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + $logPath = Join-Path -Path (Get-Location) -ChildPath 'InstallIncorrectWindowsFeatureLog' + + try + { + Configuration $configurationName + { + Import-DscResource -ModuleName xPSDesiredStateConfiguration + + xWindowsOptionalFeatureSet feature + { + Name = @("NonExistentWindowsOptionalFeature") + Ensure = "Present" + LogPath = $logPath + } + } + + { & $configurationName -OutputPath $configurationPath } | Should Not Throw + + # This should not work. LCM is expected to print errors, + # but the call to this function itself should not throw errors. + { + $startParams = @{ + Path = $configurationPath + Wait = $true + Force = $true + ErrorAction = 'SilentlyContinue' + } + Start-DscConfiguration @startParams + } | Should Not Throw + + Test-Path -Path $logPath | Should Be $true + } + finally + { + if (Test-Path -Path $logPath) + { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } +} diff --git a/Tests/Unit/MSFT_xWindowsOptionalFeature.Tests.ps1 b/Tests/Unit/MSFT_xWindowsOptionalFeature.Tests.ps1 index 1176c67d1..0cb28758e 100644 --- a/Tests/Unit/MSFT_xWindowsOptionalFeature.Tests.ps1 +++ b/Tests/Unit/MSFT_xWindowsOptionalFeature.Tests.ps1 @@ -15,7 +15,7 @@ Import-Module .\DSCResource.Tests\TestHelper.psm1 -Force $TestEnvironment = Initialize-TestEnvironment ` -DSCModuleName $Global:DSCModuleName ` -DSCResourceName $Global:DSCResourceName ` - -TestType Unit + -TestType Unit #endregion # Begin Testing @@ -29,20 +29,20 @@ try $testFeatureName = 'TestFeature'; $fakeEnabledFeature = [PSCustomObject] @{ Name = $testFeatureName; State = 'Enabled'; } $fakeDisabledFeature = [PSCustomObject] @{ Name = $testFeatureName; State = 'Disabled'; } - - Describe "$($Global:DSCResourceName)\ConvertStateToEnsure" { + + Describe "$($Global:DSCResourceName)\Convert-FeatureStateToEnsure" { It 'Returns "Present" when state is "Enabled"' { - ConvertStateToEnsure -State 'Enabled' | Should Be 'Present'; + Convert-FeatureStateToEnsure -State 'Enabled' | Should Be 'Present'; } It 'Returns "Absent" when state is "Disabled"' { - ConvertStateToEnsure -State 'Disabled' | Should Be 'Absent'; + Convert-FeatureStateToEnsure -State 'Disabled' | Should Be 'Absent'; } - } #end Describe ConvertStateToEnsure + } #end Describe Convert-FeatureStateToEnsure - Describe "$($Global:DSCResourceName)\ValidatePrerequisites" { + Describe "$($Global:DSCResourceName)\Assert-ResourcePrerequisitesValid" { $fakeWindows7 = [PSCustomObject] @{ ProductType = 1; BuildNumber = 7601; } $fakeServer2008R2 = [PSCustomObject] @{ ProductType = 2; BuildNumber = 7601; } @@ -54,49 +54,49 @@ try Mock Get-CimInstance -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' } -MockWith { return $fakeServer2008R2; } Mock Import-Module -ParameterFilter { $Name -eq 'Dism' } -MockWith { } - { ValidatePrerequisites } | Should Throw; + { Assert-ResourcePrerequisitesValid } | Should Throw; } It 'Throws when server operating system is Server 2012' { Mock Get-CimInstance -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' } -MockWith { return $fakeServer2012; } Mock Import-Module -ParameterFilter { $Name -eq 'Dism' } -MockWith { } - { ValidatePrerequisites } | Should Throw; + { Assert-ResourcePrerequisitesValid } | Should Throw; } It 'Throws when DISM module is not available' { Mock Import-Module -ParameterFilter { $Name -eq 'Dism' } -MockWith { Write-Error 'Cannot find module'; } - { ValidatePrerequisites } | Should Throw; + { Assert-ResourcePrerequisitesValid } | Should Throw; } It 'Does not throw when desktop operating system is Windows 7' { Mock Get-CimInstance -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' } -MockWith { return $fakeWindows7; } Mock Import-Module -ParameterFilter { $Name -eq 'Dism' } -MockWith { } - { ValidatePrerequisites } | Should Not Throw; + { Assert-ResourcePrerequisitesValid } | Should Not Throw; } It 'Does not throw when desktop operating system is Windows 8.1' { Mock Get-CimInstance -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' } -MockWith { return $fakeWindows81; } Mock Import-Module -ParameterFilter { $Name -eq 'Dism' } -MockWith { } - { ValidatePrerequisites } | Should Not Throw; + { Assert-ResourcePrerequisitesValid } | Should Not Throw; } It 'Does not throw when server operating system is Server 2012 R2' { Mock Get-CimInstance -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' } -MockWith { return $fakeServer2012R2; } Mock Import-Module -ParameterFilter { $Name -eq 'Dism' } -MockWith { } - { ValidatePrerequisites } | Should Not Throw; + { Assert-ResourcePrerequisitesValid } | Should Not Throw; } - } #end Describe ValidatePrerequisites + } #end Describe Assert-ResourcePrerequisitesValid Describe "$($Global:DSCResourceName)\Get-TargetResource" { - + It 'Returns [System.Collections.Hashtable] object type' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeEnabledFeature; } $targetResource = Get-TargetResource -Name $testFeatureName; @@ -104,8 +104,8 @@ try $targetResource -is [System.Collections.Hashtable] | Should Be $true; } - It 'Calls "ValidateProperties" method' { - Mock ValidatePrerequisites -MockWith { } + It 'Calls "Assert-ResourcePrerequisitesValid" method' { + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeEnabledFeature; } $targetResource = Get-TargetResource -Name $testFeatureName; @@ -114,7 +114,7 @@ try } It 'Returns "Present" when optional feature is enabled' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeEnabledFeature; } $targetResource = Get-TargetResource -Name $testFeatureName; @@ -123,20 +123,20 @@ try } It 'Returns "Absent" when optional feature is enabled' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeDisabledFeature; } $targetResource = Get-TargetResource -Name $testFeatureName; $targetResource.Ensure | Should Be 'Absent'; } - + } #end Describe Get-TargetResource Describe "$($Global:DSCResourceName)\Test-TargetResource" { It 'Returns a "[System.Boolean]" object type' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeEnabledFeature; } $targetResource = Test-TargetResource -Name $testFeatureName; @@ -145,16 +145,16 @@ try } It 'Returns false when optional feature is not available and "Ensure" = "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { } $targetResource = Test-TargetResource -Name $testFeatureName -Ensure Present; $targetResource | Should Be $false; } - + It 'Returns false when optional feature is disabled and "Ensure" = "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeDisabledFeature; } $targetResource = Test-TargetResource -Name $testFeatureName -Ensure Present; @@ -163,7 +163,7 @@ try } It 'Returns true when optional feature is enabled and "Ensure" = "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeEnabledFeature; } $targetResource = Test-TargetResource -Name $testFeatureName -Ensure Present; @@ -172,7 +172,7 @@ try } It 'Returns true when optional feature is not available and "Ensure" = "Absent"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { } $targetResource = Test-TargetResource -Name $testFeatureName -Ensure Absent; @@ -181,7 +181,7 @@ try } It 'Returns true when optional feature is disabled and "Ensure" = "Absent"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeDisabledFeature; } $targetResource = Test-TargetResource -Name $testFeatureName -Ensure Absent; @@ -190,7 +190,7 @@ try } It 'Returns false when optional feature is enabled and "Ensure" = "Absent"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Get-WindowsOptionalFeature { $FeatureName -eq $testFeatureName } -MockWith { return $fakeEnabledFeature; } $targetResource = Test-TargetResource -Name $testFeatureName -Ensure Absent; @@ -201,63 +201,63 @@ try } #end Describe Test-TargetResource Describe "$($Global:DSCResourceName)\Set-TargetResource" { - + It 'Calls "Enable-WindowsOptionalFeature" with default "NoRestart" when "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Enable-WindowsOptionalFeature -ParameterFilter { $NoRestart -eq $true } -MockWith { } - + Set-TargetResource -Name $testFeatureName; Assert-MockCalled Dism\Enable-WindowsOptionalFeature -ParameterFilter { $NoRestart -eq $true } -Scope It } It 'Calls "Enable-WindowsOptionalFeature" with "Online" when "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Enable-WindowsOptionalFeature -ParameterFilter { $Online -eq $true } -MockWith { } - + Set-TargetResource -Name $testFeatureName; Assert-MockCalled Dism\Enable-WindowsOptionalFeature -ParameterFilter { $Online -eq $true } -Scope It } It 'Calls "Enable-WindowsOptionalFeature" with default "WarningsInfo" logging level when "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'WarningsInfo' } -MockWith { } - + Set-TargetResource -Name $testFeatureName Assert-MockCalled Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'WarningsInfo' } -Scope It } It 'Calls "Enable-WindowsOptionalFeature" with "Errors" logging level when "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'Errors' } -MockWith { } - + Set-TargetResource -Name $testFeatureName -LogLevel ErrorsOnly Assert-MockCalled Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'Errors' } -Scope It } It 'Calls "Enable-WindowsOptionalFeature" with "Warnings" logging level when "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'Warnings' } -MockWith { } - + Set-TargetResource -Name $testFeatureName -LogLevel ErrorsAndWarning Assert-MockCalled Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'Warnings' } -Scope It } - + It 'Calls "Enable-WindowsOptionalFeature" without "LimitAccess" by default when "Present"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LimitAccess -eq $null } -MockWith { } Set-TargetResource -Name $testFeatureName Assert-MockCalled Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LimitAccess -eq $null } -Scope It } - + It 'Calls "Enable-WindowsOptionalFeature" with "LimitAccess" when NoWindowsUpdateCheck is specified' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Enable-WindowsOptionalFeature -ParameterFilter { $LimitAccess -eq $true } -MockWith { } Set-TargetResource -Name $testFeatureName -NoWindowsUpdateCheck $true @@ -266,16 +266,16 @@ try } It 'Calls "Disable-WindowsOptionalFeature" with default "WarningsInfo" logging level when "Absent"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Disable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'WarningsInfo' } -MockWith { } - + Set-TargetResource -Name $testFeatureName -Ensure Absent; Assert-MockCalled Dism\Disable-WindowsOptionalFeature -ParameterFilter { $LogLevel -eq 'WarningsInfo' } -Scope It } It 'Calls "Disable-WindowsOptionalFeature" with "NoRestart" when "Absent"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Disable-WindowsOptionalFeature -ParameterFilter { $NoRestart -eq $true } -MockWith { } Set-TargetResource -Name $testFeatureName -Ensure Absent; @@ -284,18 +284,18 @@ try } It 'Calls "Disable-WindowsOptionalFeature" with "Online" when "Absent"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Disable-WindowsOptionalFeature -ParameterFilter { $Online -eq $true } -MockWith { } - + Set-TargetResource -Name $testFeatureName -Ensure Absent; Assert-MockCalled Dism\Disable-WindowsOptionalFeature -ParameterFilter { $Online -eq $true } -Scope It } It 'Calls "Disable-WindowsOptionalFeature" with "Remove" when "Absent" and "RemoveFilesOnDisable"' { - Mock ValidatePrerequisites -MockWith { } + Mock Assert-ResourcePrerequisitesValid -MockWith { } Mock Dism\Disable-WindowsOptionalFeature -ParameterFilter { $Remove -eq $true } -MockWith { } - + Set-TargetResource -Name $testFeatureName -Ensure Absent -RemoveFilesOnDisable $true; Assert-MockCalled Dism\Disable-WindowsOptionalFeature -ParameterFilter { $Remove -eq $true } -Scope It From 8783dcf4ffd64a6763c8e602d7ab710fd501995c Mon Sep 17 00:00:00 2001 From: Daniel Krebs Date: Mon, 5 Sep 2016 11:06:00 +0700 Subject: [PATCH 2/2] Updates as per PR comments --- .../MSFT_xProcessResource.psm1 | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index c7fba4f51..8c5cc2960 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -137,7 +137,7 @@ function Split-Credential Gets the state of the managed process. .PARAMETER Path - The path to the process executable. If this the file name of the executable + The path to the process executable. If this is the file name of the executable (not the fully qualified path), the DSC resource will search the environment Path variable ($env:Path) to find the executable file. If the value of this property is a fully qualified path, DSC will not use the Path environment variable to find the file, and will throw an @@ -221,7 +221,7 @@ function Get-TargetResource Ensures the managed process executable is Present or Absent. .PARAMETER Path - The path to the process executable. If this the file name of the executable + The path to the process executable. If this is the file name of the executable (not the fully qualified path), the DSC resource will search the environment Path variable ($env:Path) to find the executable file. If the value of this property is a fully qualified path, DSC will not use the Path environment variable to find the file, and will throw an @@ -293,12 +293,12 @@ function Set-TargetResource if ($null -ne $PsDscContext.RunAsUser) { - $params = @{ + $newInvalidArgumentExceptionParams = @{ ArgumentName = 'PsDscRunAsCredential' Message = ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) } - New-InvalidArgumentException @params + New-InvalidArgumentException @newInvalidArgumentExceptionParams } $Path = Expand-Path -Path $Path @@ -317,12 +317,12 @@ function Set-TargetResource if ($Ensure -eq 'Absent') { - $params = @{ + $assertHashtableParams = @{ Hashtable = $PSBoundParameters Key = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) } - Assert-HashtableDoesNotContainKey @params + Assert-HashtableDoesNotContainKey @assertHashtableParams $whatIfShouldProcess = $PSCmdlet.ShouldProcess($Path, $LocalizedData.StoppingProcessWhatif) if ($win32Processes.Count -gt 0 -and $whatIfShouldProcess) @@ -370,11 +370,11 @@ function Set-TargetResource { if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument])) { - $params = @{ + $assertPathArgumentRootedParams = @{ PathArgumentName = $shouldBeRootedPathArgument PathArgument = $PSBoundParameters[$shouldBeRootedPathArgument] } - Assert-PathArgumentRooted @params + Assert-PathArgumentRooted @assertPathArgumentRootedParams } } @@ -384,11 +384,11 @@ function Set-TargetResource { if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument])) { - $params = @{ + $assertPathArgumentValidParams = @{ PathArgumentName = $shouldExistPathArgument PathArgument = $PSBoundParameters[$shouldExistPathArgument] } - Assert-PathArgumentValid @params + Assert-PathArgumentValid @assertPathArgumentValidParams } } @@ -409,10 +409,10 @@ function Set-TargetResource foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) { $parameterKey = $startProcessOptionalArgumentMap[$startProcessOptionalArgumentName] - $value = $PSBoundParameters[$parameterKey] - if (-not [String]::IsNullOrEmpty($value)) + $parameterValue = $PSBoundParameters[$parameterKey] + if (-not [String]::IsNullOrEmpty($parameterValue)) { - $startProcessArguments[$startProcessOptionalArgumentName] = $value + $startProcessArguments[$startProcessOptionalArgumentName] = $parameterValue } } @@ -437,11 +437,11 @@ function Set-TargetResource { foreach ($key in @('StandardOutputPath','StandardInputPath','WorkingDirectory')) { - $params = { + $newInvalidArgumentExceptionParams = { ArgumentName = $key Message = $LocalizedData.ErrorParametersNotSupportedWithCredential } - New-InvalidArgumentException @params + New-InvalidArgumentException @newInvalidArgumentExceptionParams } $splitCredentialResult = Split-Credential $Credential @@ -504,7 +504,7 @@ function Set-TargetResource Tests if the managed process is Present or Absent. .PARAMETER Path - The path to the process executable. If this the file name of the executable + The path to the process executable. If this is the file name of the executable (not the fully qualified path), the DSC resource will search the environment Path variable ($env:Path) to find the executable file. If the value of this property is a fully qualified path, DSC will not use the Path environment variable to find the file, and will throw an @@ -518,22 +518,33 @@ function Set-TargetResource Indicates the credentials for starting the process. .PARAMETER Ensure - Indicates if the process exists. Set this property to "Present" to ensure that the process - exists. Otherwise, set it to "Absent". The default is "Present". + Indicates if the process exists. Set this property to "Present" to return true + if the process that the process exists. Otherwise, set it to "Absent" to return true if + the process does not exist. The default is "Present". .PARAMETER StandardOutputPath - Indicates the location to write the standard output. Any existing file there will be - overwritten. + Indicates the location to write the standard output. + + Note: The value provided to this parameter is not being used inside the function + because we only test if the managed process is Present or Absent. .PARAMETER StandardErrorPath - Indicates the directory path to write the standard error. Any existing file there will be - overwritten. + Indicates the directory path to write the standard error. + + Note: The value provided to this parameter is not being used inside the function + because we only test if the managed process is Present or Absent. .PARAMETER StandardInputPath Indicates the standard input location. + Note: The value provided to this parameter is not being used inside the function + because we only test if the managed process is Present or Absent. + .PARAMETER WorkingDirectory Indicates the location that will be used as the current working directory for the process. + + Note: The value provided to this parameter is not being used inside the function + because we only test if the managed process is Present or Absent. #> function Test-TargetResource { @@ -577,12 +588,12 @@ function Test-TargetResource if ($null -ne $PsDscContext.RunAsUser) { - $params = @{ + $newInvalidArgumentExceptionParams = @{ ArgumentName = 'PsDscRunAsCredential' Message = ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) } - New-InvalidArgumentException @params + New-InvalidArgumentException @newInvalidArgumentExceptionParams } $Path = Expand-Path -Path $Path @@ -745,12 +756,12 @@ function Get-Win32Process if ($process.Path -ieq $Path) { Write-Verbose -Message ($LocalizedData.VerboseInProcessHandle -f $process.Id) - $params = @{ + $getCimInstanceParams = @{ ClassName = 'Win32_Process' Filter = "ProcessId = $($process.Id)" ErrorAction = 'SilentlyContinue' } - $processes += Get-CimInstance @params + $processes += Get-CimInstance @getCimInstanceParams } } }