DSCResources/MSFT_xEnvironmentResource/MSFT_xEnvironmentResource.psm1
# This PS module contains functions for Desired State Configuration (DSC) "xEnvironment" resource # Fallback message strings in en-US DATA localizedData { # culture = "en-US" ConvertFrom-StringData @' EnvVarCreated = (CREATE) Environment variable '{0}' with value '{1}' EnvVarSetError = (ERROR) Failed to set environment variable '{0}' to value '{1}' EnvVarPathSetError = (ERROR) Failed to add path '{0}' to environment variable '{1}' holding value '{2}' EnvVarRemoveError = (ERROR) Failed to remove environment variable '{0}' holding value '{1}' EnvVarPathRemoveError = (ERROR) Failed to remove path '{0}' from variable '{1}' holding value '{2}' EnvVarUnchanged = (UNCHANGED) Environment variable '{0}' with value '{1}' EnvVarUpdated = (UPDATE) Environment variable '{0}' from value '{1}' to value '{2}' EnvVarPathUnchanged = (UNCHANGED) Path environment variable '{0}' with value '{1}' EnvVarPathUpdated = (UPDATE) Environment variable '{0}' from value '{1}' to value '{2}' EnvVarNotFound = (NOT FOUND) Environment variable '{0}' EnvVarFound = (FOUND) Environment variable '{0}' with value '{1}' EnvVarFoundWithMisMatchingValue = (FOUND MISMATCH) Environment variable '{0}' with value '{1}' mismatched the specified value '{2}' EnvVarRemoved = (REMOVE) Environment variable '{0}' '@ } # Commented-out until more languages are supported # Import-LocalizedData LocalizedData -filename MSFT_xEnvironmentResource.strings.psd1 #------------------------------------- # Script-level Constants and Variables #------------------------------------- $EnvVarRegPathMachine = "HKLM:\\System\\CurrentControlSet\\Control\\Session Manager\\Environment" $EnvVarRegPathUser = "HKCU:\\Environment" $EnvironmentVariableTarget = @{ Process = 0; User = 1; Machine = 2 } $MaxSystemEnvVariableLength = 1024 $MaxUserEnvVariableLength = 255 Function Throw-InvalidArgumentException { param( [string] $Message, [string] $ParamName ) $exception = new-object System.ArgumentException $Message,$ParamName $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null throw $errorRecord } function GetEnvironmentVariable { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $Name, [parameter(Mandatory = $true)] [int] $Target ) if ($Target -eq $EnvironmentVariableTarget.Process) { return [System.Environment]::GetEnvironmentVariable($Name); } if ($Target -eq $EnvironmentVariableTarget.Machine) { $retVal = Get-ItemProperty $EnvVarRegPathMachine -Name $Name -ErrorAction SilentlyContinue return $retVal.$Name } if ($Target -eq $EnvironmentVariableTarget.User) { $retVal = Get-ItemProperty $EnvVarRegPathUser -Name $Name -ErrorAction SilentlyContinue return $retVal.$Name } } function SetEnvironmentVariable { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] $Name, [String] $Value, [parameter(Mandatory = $true)] [int] $Target ) if ($Target -eq $EnvironmentVariableTarget.Process) { [System.Environment]::SetEnvironmentVariable($Name, $Value); } if ($Target -eq $EnvironmentVariableTarget.Machine) { if ($Name.Length -ge $MaxSystemEnvVariableLength) { Throw-InvalidArgumentException -Message "Argument is too long." -ParamName $Name } $Path = $EnvVarRegPathMachine } elseif ($Target -eq $EnvironmentVariableTarget.User) { if ($Name.Length -ge $MaxUserEnvVariableLength) { Throw-InvalidArgumentException -Message "Argument is too long." -ParamName $Name } $Path = $EnvVarRegPathUser } $environmentKey = Get-ItemProperty $Path -Name $Name -ErrorAction SilentlyContinue if ($environmentKey) { if (!$Value) { Remove-ItemProperty $Path -Name $Name -ErrorAction SilentlyContinue } else { Set-ItemProperty $Path -Name $Name -Value $Value -ErrorAction SilentlyContinue } } } #------------------------------ # The Get-TargetResource cmdlet #------------------------------ FUNCTION Get-TargetResource { [OutputType([Hashtable])] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name ) $retVal = GetItemProperty $EnvVarRegPathMachine -Name $Name -Expand:$false -ErrorAction SilentlyContinue if($retVal -eq $null) { Write-Verbose ($localizedData.EnvVarNotFound -f $Name) return @{Ensure='Absent'; Name=$Name} } Write-Verbose ($localizedData.EnvVarFound -f $Name, $retVal.$Name) return @{Ensure='Present'; Name=$Name; Value=$retVal.$Name} } function Set-EnvVar { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [ValidateNotNull()] [System.String] $Value = [String]::Empty ) $err = Set-ItemProperty $EnvVarRegPathMachine -Name $Name -Value $Value 2>&1 if($err) { Write-Verbose ($localizedData.EnvVarSetError -f $Name, $Value) throw $err } try { if($value) { SetEnvironmentVariable -Name $Name -Value $Value -Target $EnvironmentVariableTarget.Machine SetEnvironmentVariable -Name $Name -Value $Value -Target $EnvironmentVariableTarget.Process } } catch { Write-Verbose ($localizedData.EnvVarSetError -f $Name, $Value) throw $_ } } function Remove-EnvVar { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name ) $curVarProperties = Get-ItemProperty $EnvVarRegPathMachine -Name $Name -ErrorAction SilentlyContinue $currentValueFromEnv = GetEnvironmentVariable -Name $name -Target $EnvironmentVariableTarget.Process if($curVarProperties -ne $null) { $err = Remove-ItemProperty $EnvVarRegPathMachine -Name $Name 2>&1 if($err) { Write-Log -Message ($localizedData.EnvVarRemoveError -f $Name, $Value) throw $err } } if($currentValueFromEnv -ne $null) { try { SetEnvironmentVariable -Name $Name -Value $null -Target $EnvironmentVariableTarget.Machine SetEnvironmentVariable -Name $Name -Value $null -Target $EnvironmentVariableTarget.Process } catch { Write-Verbose ($localizedData.EnvVarRemoveError -f $Name, $Value) throw $_ } } } #------------------------------ # The Set-TargetResource cmdlet #------------------------------ FUNCTION Set-TargetResource { [CmdletBinding(SupportsShouldProcess=$true)] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [ValidateNotNull()] [System.String] $Value = [String]::Empty, [ValidateSet("Present", "Absent")] [System.String] $Ensure = "Present", [System.Boolean] $Path = $false ) $ValueSpecified = $PSBoundParameters.ContainsKey("Value") $curVarProperties = GetItemProperty $EnvVarRegPathMachine -Name $Name -Expand:(-not $Path) -ErrorAction SilentlyContinue $currentValueFromEnv = GetEnvironmentVariable -Name $name -Target $EnvironmentVariableTarget.Process # ---------------- # ENSURE = PRESENT if ($Ensure -ieq "Present") { if (($curVarProperties -eq $null) -or (($currentValueFromEnv -eq $null) -and ($curVarProperties.$Name -ne [string]::Empty))) # The specified variable doesn't exist already { # Given the specified $Name environment variable doesn't exist already, # simply create one with the specified value and return. If no $Value is # specified, the default value is set to empty string "" (per spec). # Both path and non-path cases are covered by this. $successMessage = $localizedData.EnvVarCreated -f $Name, $Value if ($PSCmdlet.ShouldProcess($successMessage, $null, $null)) { Set-EnvVar -Name $Name -Value $Value } return } # If the control reaches here, the specified variable exists already if (!$ValueSpecified) { # Given no $Value was specified to be set and the variable exists, # we'll leave the existing variable as is. # This covers both path and non-path variables. Write-Log -Message ($localizedData.EnvVarUnchanged -f $Name, $curVarProperties.$Name) return } # If the control reaches here: the specified variable exists already and a $Value has been specified to be set. if (!$Path) { # For non-path variables, simply set the specified $Value as the new value of the specified # variable $Name, then return. $successMessage = $localizedData.EnvVarUpdated -f $Name, $curVarProperties.$Name, $Value if ($Value -ceq $curVarProperties.$Name) { $successMessage = $localizedData.EnvVarUnchanged -f $Name, $curVarProperties.$Name } if ($PSCmdlet.ShouldProcess($successMessage, $null, $null) -and ($Value -cne $curVarProperties.$Name)) { Set-EnvVar -Name $Name -Value $Value } return } # If the control reaches here: the specified variable exists already, it is a path variable and a $Value has been specified to be set. # Check if an empty, whitespace or semi-colon only string has been specified. If yes, return unchanged. $trimmedValue = $Value.Trim(";"," ") if ([String]::IsNullOrEmpty($trimmedValue)) { Write-Log -Message ($localizedData.EnvVarPathUnchanged -f $Name, $curVarProperties.$Name) return } $setValue = $curVarProperties.$Name + ";" $specifiedPaths = $trimmedValue -split ";" $currentPaths = $curVarProperties.$Name -split ";" $varUpdated = $false foreach ($specifiedPath in $specifiedPaths) { if (FindSubPath -QueryPath $specifiedPath -PathList $currentPaths) { # Found this $specifiedPath as one of the $currentPaths, no need to add this again, skip/continue to the next $specifiedPath continue } # If the control reached here, we didn't find this $specifiedPath in the $currentPaths, add it # and mark the environment variable as updated. $varUpdated = $true $setValue += $specifiedPath + ";" } # Remove any extraneous ";" at the end (and potentially start - as a side-effect) of the value to be set $setValue = $setValue.Trim(";") # Set the expected success message $successMessage = $localizedData.EnvVarPathUnchanged -f $Name, $curVarProperties.$Name if ($varUpdated) { $successMessage = $localizedData.EnvVarPathUpdated -f $Name, $curVarProperties.$Name, $setValue } if ($PSCmdlet.ShouldProcess($successMessage, $null, $null)) { # Finally update the existing environment path variable Set-EnvVar -Name $Name -Value $setValue } } # --------------- # ENSURE = ABSENT elseif ($Ensure -ieq "Absent") { if(($curVarProperties -eq $null) -and ($currentValueFromEnv -eq $null)) { # Variable not found, condition is satisfied and there is nothing to set/remove, return Write-Log -Message ($localizedData.EnvVarNotFound -f $Name) return } if(!$ValueSpecified -or !$Path) { # If no $Value specified to be removed, simply remove the environment variable (holds true for both path and non-path variables # OR # Regardless of $Value, if the target variable is a non-path variable, simply remove it to meet the absent condition $successMessage = $localizedData.EnvVarRemoved -f $Name if ($PSCmdlet.ShouldProcess($successMessage, $null, $null)) { Remove-EnvVar -Name $Name } return } # If the control reaches here: target variable is an existing environment path-variable and a specified $Value needs be removed from it # Check if an empty string or semi-colon only string has been specified as $Value. If yes, return unchanged as we don't need to remove anything. $trimmedValue = $Value.Trim(";") if ([String]::IsNullOrEmpty($trimmedValue)) { Write-Log -Message ($localizedData.EnvVarPathUnchanged -f $Name, $curVarProperties.$Name) return } $finalPath = "" $specifiedPaths = $trimmedValue -split ";" $currentPaths = $curVarProperties.$Name -split ";" $varAltered = $false foreach ($subpath in $currentPaths) { if (FindSubPath -QueryPath $subpath -PathList $specifiedPaths) { # Found this $subpath as one of the $specifiedPaths, skip adding this to the final value/path of this variable # and mark the variable as altered. $varAltered = $true continue } # If the control reaches here, the current $subpath was not part of the $specifiedPaths (to be removed), # so keep this $subpath in the finalPath $finalPath += $subpath + ";" } # Remove any extraneous ";" at the end (and potentially start - as a side-effect) of the $finalPath $finalPath = $finalPath.Trim(";") # Set the expected success message $successMessage = $localizedData.EnvVarPathUnchanged -f $Name, $curVarProperties.$Name if ($varAltered) { $successMessage = $localizedData.EnvVarPathUpdated -f $Name, $curVarProperties.$Name, $finalPath if ([String]::IsNullOrEmpty($finalPath)) { $successMessage = $localizedData.EnvVarRemoved -f $Name } } # Handle WhatIf case and update resource as appropriate if ($PSCmdlet.ShouldProcess($successMessage, $null, $null)) { # Finally, update the environment path-variable if ([String]::IsNullOrEmpty($finalPath)) { Remove-EnvVar -Name $Name } else { Set-EnvVar -Name $Name -Value $finalPath } if($err) { Write-Log -Message ($localizedData.EnvVarPathRemoveError -f $Value, $Name, $curVarProperties.$Name) throw $err } } } } #------------------------------- # The Test-TargetResource cmdlet #------------------------------- FUNCTION Test-TargetResource { [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Name, [ValidateNotNull()] [System.String] $Value, [ValidateSet("Present", "Absent")] [System.String] $Ensure = "Present", [System.Boolean] $Path = $false ) $ValueSpecified = $PSBoundParameters.ContainsKey("Value") $curVarProperties = GetItemProperty $EnvVarRegPathMachine -Name $Name -Expand:(-not $Path) -ErrorAction SilentlyContinue $currentValueFromEnv = GetEnvironmentVariable -Name $name -Target $EnvironmentVariableTarget.Process # ---------------- # ENSURE = PRESENT if ($Ensure -ieq "Present") { if (($curVarProperties -eq $null) -or (($currentValueFromEnv -eq $null) -and ($curVarProperties.$Name -ne [string]::Empty)) ) { # Variable not found, return failure Write-Verbose ($localizedData.EnvVarNotFound -f $Name) return $false } if (!$ValueSpecified) { # No value has been specified for test, so the existence of the variable means success Write-Verbose ($localizedData.EnvVarFound -f $Name, $curVarProperties.$Name) return $true } if (!$Path) { # For this non-path variable, make sure that the specified $Value matches the current value. # Success if it matches, failure otherwise if ($Value -ceq $curVarProperties.$Name) { Write-Verbose ($localizedData.EnvVarFound -f $Name, $curVarProperties.$Name) return $true } else { Write-Verbose ($localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $curVarProperties.$Name, $Value) return $false } } # If the control reaches here, the expected environment variable exists, it is a path variable and a $Value is specified to test against if (FindPath -ExistingPaths $curVarProperties.$Name -QueryPaths $Value -FindCriteria All) { # The specified path was completely present in the existing environment variable, return success Write-Verbose ($localizedData.EnvVarFound -f $Name, $curVarProperties.$Name) return $true } # If the control reached here some part of the specified path ($Value) was not found in the existing variable, return failure Write-Verbose ($localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $curVarProperties.$Name, $Value) return $false } # --------------- # ENSURE = ABSENT elseif ($Ensure -eq "Absent") { if(($curVarProperties -eq $null) -and ($currentValueFromEnv -eq $null)) { # Variable not found (path/non-path and $Value both do not matter then), return success Write-Verbose ($localizedData.EnvVarNotFound -f $Name) return $true } if (!$ValueSpecified) { # Given no value has been specified for test, the mere existence of the variable fails the test Write-Verbose ($localizedData.EnvVarFound -f $Name, $curVarProperties.$Name) return $false } # If the control reaches here: the variable exists and a value has been specified to test against it if (!$Path) { # For this non-path variable, make sure that the specified value doesn't match the current value # Success if it doesn't match, failure otherwise if ($Value -cne $curVarProperties.$Name) { Write-Verbose ($localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $curVarProperties.$Name, $Value) return $true } else { Write-Verbose ($localizedData.EnvVarFound -f $Name, $curVarProperties.$Name) return $false } } # If the control reaches here: the variable exists, it is a path variable, and a value has been specified to test against it if (FindPath -ExistingPaths $curVarProperties.$Name -QueryPaths $Value -FindCriteria Any) { # One of the specified paths in $Value exists in the environment variable path, thus the test fails Write-Verbose ($localizedData.EnvVarFound -f $Name, $curVarProperties.$Name) return $false } # If the control reached here, none of the specified paths were found in the existing path-variable, return success Write-Verbose ($localizedData.EnvVarFoundWithMisMatchingValue -f $Name, $curVarProperties.$Name, $Value) return $true } } #---------------------------------------- # Utility to write WhatIf or Verbose logs #---------------------------------------- FUNCTION Write-Log { [CmdletBinding(SupportsShouldProcess=$true)] param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Message ) if ($PSCmdlet.ShouldProcess($Message, $null, $null)) { Write-Verbose $Message } } #----------------------------------- # Utility to match environment paths #----------------------------------- FUNCTION FindPath { param ( [System.String] $ExistingPaths, [System.String] $QueryPaths, [parameter(Mandatory = $true)] [ValidateSet("Any", "All")] [System.String] $FindCriteria ) $existingPathList = $ExistingPaths -split ";" $queryPathList = $QueryPaths -split ";" switch ($FindCriteria) { "Any" { foreach ($queryPath in $queryPathList) { if (FindSubPath -QueryPath $queryPath -PathList $existingPathList) { # Found this $queryPath in the existing paths, return $true return $true } } # If the control reached here, none of the $QueryPaths were found as part of the $ExistingPaths, return $false return $false } "All" { foreach ($queryPath in $queryPathList) { $found = $false if($queryPath) { if (!(FindSubPath -QueryPath $queryPath -PathList $existingPathList)) { # The current $queryPath wasn't found in any of the $existingPathList, return failure return $false } } } # If the control reached here, all of the $QueryPaths were found as part of the $ExistingPaths, return $true return $true } } } #--------------------------------------- # Utility to search a path in a pathlist #--------------------------------------- FUNCTION FindSubPath { param ( [System.String] $QueryPath, [String[]] $PathList ) foreach ($path in $PathList) { if($QueryPath -ieq $path) { # If the query path matches any of the paths in $PathList, return $true return $true } } return $false } #--------------------------------------------------------------- # Utility to get item property without expanding it if necessary #--------------------------------------------------------------- FUNCTION GetItemProperty { param ( [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $Path, [ValidateNotNull()] [System.String] $Name, [switch] $Expand = $false ) if ($Expand) { return (Get-ItemProperty $EnvVarRegPathMachine -Name $Name -ErrorAction SilentlyContinue) } else { if (!(Test-Path -Path $Path)) { return $null; } $PathTokens = $Path.Split('\',[System.StringSplitOptions]::RemoveEmptyEntries) $Division = $PathTokens[0].Replace(':', '') $Entry = $PathTokens[1..($PathTokens.Count-1)] -join '\' # Since the target registry path coming to this function is hardcoded for local machine $Hive = [Microsoft.Win32.Registry]::LocalMachine $NoteProperties = @{} try { $Key = $Hive.OpenSubKey($Entry) $ValueNames = $Key.GetValueNames() if ($ValueNames -inotcontains $Name) { return $null } [string] $Value = $Key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) $NoteProperties.Add($Name, $Value) } finally { if ($key) { $key.Close() } } [System.Management.Automation.PSObject] $PropertyResults = New-Object -TypeName System.Management.Automation.PSObject -Property $NoteProperties return $PropertyResults } } Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource |