How can I make the WindowsProcess resource idempotent?

How can I make a WindowsProcess resource idempotent, so that I can run it many times without failing? Here is a specific example (but my question is more in general):

WindowsProcess CreateTestShare { Path = "C:\windows\System32\net.exe" Arguments = "share TestShare=c:\testshare /GRANT:username,FULL" Ensure = "Present" } I'm using this code to create a new share (before you ask, I can't use xSmbShare because it doesn't work on Windows 7). It works great the first time I run it, but the second time I run it it fails (because the share already exists, so 'net share' throws an error!).

This is where my general question comes up. How can I make WindowsProcess calls idempotent? It seems to me that the ‘Ensure’ setting is practically useless in this scenario. It will only start net.exe if it’s not already running (but since net.exe does its thing and then exits, it will almost never be running).

I can easily write a cmd.exe or Powershell snippet to verify if the share already exists, but DSC has no mechanism that I know of to let me put that into my resource call. Those familiar with Chef will know that it has ‘only_if’ and ‘not_if’ Guards that one can use to make anything idempotent.

So, how would you tackle this problem?

Process is the wrong resource to use here. As you’ve pointed out, you’re telling DSC that there should always be an instance of “net.exe” running, which doesn’t really make any sense.

What you would need is a new version of xSmbShare which is compatible with older operating systems. It might use “net share” in its Set/Get/Test-TargetResource function, but that’s an implementation detail that the DSC engine doesn’t really need to know about.

I would use the Script resource instead. You can use a PowerShell snippet to check if the share exists (GetScript) and your net.exe call as SetScript.

Agreed. The script resource is the way to go if you’re not building custom.

Hey Jay,

If you put together a script resource to do this (Get/Set/Test-TargetResource functions), would you mind sharing this? As you can see from my earlier post, I need to do this as well.

Thanks,
Jeff

There’s a second question in there outside of the example, which is “when and how should I make Set idempotent?” The DSC engine itself helps with this because Test is always run before Set. Still, I have found reasons to have Set functions be idempotent individually. It reduces the risk of unintended outcomes in case you should made a mistake when authoring Test, and it allows someone unfamiliar with your resource to more easily test your Set function because they can run it directly multiple times.

I also discovered that my Set functions are often becoming two-stage. “New” and “Set”, or create something and then if it exists make sure it is configured correctly. This helps to address the issue indirectly because in order to handle these cases you end up needing to check for existence. Good opportunity to add to your verbose logging to indicate whether Set actually created or configured.

In your example, obviously you wouldn’t “configure” a process, so the check for existence just decides whether or not to create it.

I guess I thought WindowsProcess was the generic way to call arbitrary scripts and programs. Are you guys saying the Script resource is more applicable for that? If that’s the case, what are some examples of what the WindowsProcess resource is for? Managing system services?

Based on your guys’ feedback, here’s what I ended up writing. It does basically the same thing as my code in the OP, but should be more ‘DSC Compliant’. Let me know if you have any feedback on it.

Script CreateShare { GetScript = { net share | findstr TestShare, return @{HasTestShare = $?} } TestScript = { return (net share | findstr TestShare), $? } SetScript = { net share TestShare=c:\testshare /GRANT:"Administrators"`,FULL } }

Here is what I cooked up in the form of a custom DSC resource. It also checks to make sure the Path is correct for the share. It doesn’t check access permissions.

<<<
<#
Summary
=======
This custom resource is used to create SmbShares. It was
created because the xSmbShare resource contained in the
Resource kit from Microsoft only works on WS2012.
#>

Fallback message strings in en-US

DATA localizedData
{
# culture = “en-US”
ConvertFrom-StringData @’
ShareNotFound = (NOT FOUND) Share not found - Name: ‘{0}’
ShareFound = (FOUND) Share found - Name: ‘{0}’
ShareFoundWithCorrectPath = (FOUND CORRECT PATH) Share found with correct path - Name: ‘{0}’, Path: ‘{1}’
ShareFoundWithIncorrectPath = (FOUND INCORRECT PATH) Share found with incorrect path - Name: ‘{0}’, with Path: ‘{1}’ mismatched the specified Path: ‘{2}’
ShareCreated = (CREATED) Share - Name: ‘{0}’, Path: ‘{1}’, Remark: ‘{2}’, Grant: ‘{3}’
ShouldCreateShare = (SHOULD CREATE) Should create share? - Name: ‘{0}’, Path: ‘{1}’, Remark: ‘{2}’, Grant: ‘{3}’
ShareCreateError = (ERROR) Error creating share - Name: ‘{0}’, Path: ‘{1}’, ExitCode: ‘{2}’
ShareDeleted = (DELETED) Existing share deleted - Name: ‘{0}’, Path: ‘{1}’
'@
}

#------------------------------

The Get-TargetResource cmdlet

#------------------------------
function Get-TargetResource
{
[CmdletBinding()]
[OutputType([System.Collections.Hashtable])]
param
(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$Name
)

$Path = ""
$Remark = ""
$Grant = $null

$Share = Get-CimInstance -ClassName Win32_Share -Filter "Name = '$Name'"
if ($Share)
{
    Write-Verbose ($localizedData.ShareFound -f $Name)
    $Path = $Share.Path
    $Remark = $Share.Description
}
else
{
    Write-Verbose ($localizedData.ShareNotFound -f $Name)
}

return @{Name=$Name; Path=$Path; Remark=$Remark; Grant=$Grant}

}

#------------------------------

The Set-TargetResource cmdlet

#------------------------------
function Set-TargetResource
{
[CmdletBinding(SupportsShouldProcess=$true)]
param
(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$Name,

	[parameter(Mandatory = $true)]
	[ValidateNotNullOrEmpty()]
	[System.String]
	$Path,

	[System.String]
	$Remark,

	[System.String[]]
	$Grant
)

$Share = Get-CimInstance -ClassName Win32_Share -Filter "Name = '$Name'"
if ($Share)
{
    if ($Share.Path -eq $path)
    {
        Write-Verbose ($localizedData.ShareFoundWithCorrectPath -f $Name, $Path)
        return
    }
    else
    {
        Write-Verbose ($localizedData.ShareFoundWithIncorrectPath -f $Name, $Share.Path, $Path)
    }
}
else
{
    Write-Verbose ($localizedData.ShareNotFound -f $Name)
}

$GrantAsString = ""
if ($Grant)
{
    $GrantAsString = [System.String]::Join(";", $Grant)
}

$shouldProcessMessage = $localizedData.ShouldCreateShare -f $Name, $Path, $Remark, $GrantAsString
if ($PSCmdlet.ShouldProcess($shouldProcessMessage, $null, $null))
{
    # Delete the existing share if it exists because if we are here its path isn't correct
    if ($Share)
    {
        Remove-CimInstance -InputObject $Share
        Write-Verbose ($localizedData.ShareDeleted -f $Name, $Share.Path)
    }

    # Create the share
    $grants = ""
    if ($Grant)
    {
        foreach ($perm in $Grant)
        {
            $grants = $grants + " ""/GRANT:" + $perm + """"
        }
    }

    $param = $Name + "=""" + $Path + """ /REMARK:""" + $Remark + """" + $grants
    $command = "net share " + $param
    Invoke-Expression "$command" -ErrorAction Ignore -WarningAction Ignore 2&gt;&amp;1 | Out-Null
    if ($LASTEXITCODE -eq 0)
    {
        Write-Verbose ($localizedData.ShareCreated -f $Name, $Path, $Remark, $GrantAsString)
    }
    else
    {
        Write-Verbose ($localizedData.ShareCreateError -f $Name, $Path, $LASTEXITCODE)
    }
}

return

}

#-------------------------------

The Test-TargetResource cmdlet

#-------------------------------
<#
Function returns true if the share exists and is associated
with the correct path. It returns false if the share doesn’t
exist or if it does but is associated with a different
path. It does not validate permissions specified via the
Grant parameter.
#>
function Test-TargetResource
{
[CmdletBinding()]
[OutputType([System.Boolean])]
param
(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$Name,

	[parameter(Mandatory = $true)]
	[ValidateNotNullOrEmpty()]
	[System.String]
	$Path,

	[System.String]
	$Remark,

	[System.String[]]
	$Grant
)

$Share = Get-CimInstance -ClassName Win32_Share -Filter "Name = '$Name'"
if ($Share)
{
    if ($Share.Path -eq $path)
    {
        Write-Verbose ($localizedData.ShareFoundWithCorrectPath -f $Name, $Path)
        return $true
    }

    Write-Verbose ($localizedData.ShareFoundWithIncorrectPath -f $Name, $Share.Path, $Path)
    return $false
}
else
{
    Write-Verbose ($localizedData.ShareNotFound -f $Name)
}

return $false 

}

Export-ModuleMember -Function *-TargetResource

>>>

It’s definitely on the right track. :slight_smile: If you wanted to make it more reusable, the name of the share, path to the folder, and permissions to assign would be parameters to the resource, and the Get / Set / Test functions would work with all 3 of those.

As-is, it would be possible for someone to either change the permissions on your share, or delete your share and share some other folder with the name TestShare. In both cases, the resource wouldn’t detect or correct those conditions.

Edit: This was in reply to Jay’s smaller bit of code. Haven’t reviewed the most recent post yet.

I don’t fully understand the Get-DscConfiguration function (it keeps throwing errors for me that I can’t find answers to on the internet) so that’s why I implemented a rather bare-bones version of this.

I like the way you think though. I can easily parameterize the resource, and then beef up the Test function so that it makes sure the permissions (and shared folder) are correct. I just need to better understand how the Get and Test functions work!

Dave, I tried implementing what you discussed but ran into some weird issues. Whenever I run ‘Get-DscConfiguration’, here’s the error I got:

Get-DscConfiguration : The PowerShell provider C:\Windows\system32\WindowsPowershell\v1.0\Modules\PSDesiredStateConfiguration\DSCResources\MSFT_ScriptResource returned results
that are not valid from Get-TargetResource. The HasTestShare key is not a valid property in the corresponding provider schema file. The results from Get-TargetResource must be
in a Hashtable format. The keys in the Hashtable must be the same as the properties in the corresponding provider schema file.

When I googled the error, I eventually found this page. In it, the author did some excellent research. It turns out that the Script resource can only have 5 keys in its configuration data. They are ‘Get-Script’, ‘Test-Script’, ‘Set-Script’, ‘Result’, and ‘Credential’. Of those, ‘Result’ is really the only one you can use to store the results of your script run!

How would you propose modifying Get-DscConfiguration to do what you described above? Or would you advise only using Test and Set to achieve this?

Ah, yes. I don’t use the Script resource, and forgot about that. Personally, I would write a new resource rather than using script. You wind up writing most of the same code anyway.

Jay,

Please check out below link to a Script resource snippet just published on Gist from a Configuration I’ve created in Mid-March to configure a server to become an SCCM distribution point.