How can I make the WindowsProcess resource idempotent?

Tagged: ,

This topic contains 13 replies, has 6 voices, and was last updated by Profile photo of Daniel Krebs Daniel Krebs 2 years, 6 months ago.

  • Author
    Posts
  • #15872
    Profile photo of Jay Spang
    Jay Spang
    Participant

    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?

  • #15873
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    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.

  • #15874
    Profile photo of Daniel Krebs
    Daniel Krebs
    Participant

    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.

  • #15876
    Profile photo of Don Jones
    Don Jones
    Keymaster

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

  • #15885
    Profile photo of Jeff Pflum
    Jeff Pflum
    Participant

    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

  • #15893
    Profile photo of Michael Greene
    Michael Greene
    Participant

    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.

  • #15896
    Profile photo of Jay Spang
    Jay Spang
    Participant

    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?

  • #15943
    Profile photo of Jay Spang
    Jay Spang
    Participant

    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 }
    }

  • #15944
    Profile photo of Jeff Pflum
    Jeff Pflum
    Participant

    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>&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

    >>>

  • #15947
    Profile photo of Jay Spang
    Jay Spang
    Participant

    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!

  • #15954
    Profile photo of Jay Spang
    Jay Spang
    Participant

    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?

  • #15955
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    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.

  • #15958
    Profile photo of Daniel Krebs
    Daniel Krebs
    Participant

    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.

  • #15945
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    It's definitely on the right track. 🙂 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.

You must be logged in to reply to this topic.