none
DSC configuration data for Script resources

    Question

  • Hi,

    I'm trying to use PowerShell DSC to modify a settings file for an in-house application. I'm able to pass configuration data to File resources, but the same syntax doesn't work for Script resources and I'm stuck on how to do this.

    The following works for File resources (i.e. $Node.ConfigFolder contains the value specified in the "-ConfigurationData" parameter when I prepared the MOF file):

        File ConfigurationFolder {
                Type = "Directory"
                DestinationPath = $Node.ConfigFolder
                Ensure = "Present"
            }

    However, the same thing doesn't work for Script resources:

    Script ConfigurationFile { SetScript = { write-verbose "running ConfigurationFile.SetScript"; write-verbose "folder = $($Node.ConfigFolder)"; write-verbose "filename = $($Node.ConfigFile)"; [System.IO.File]::WriteAllText($Node.ConfigFile, "enabled=" + $Node.Enabled); } TestScript = { write-verbose "running ConfigurationFile.TestScript"; write-verbose "folder = $($Node.ConfigFolder)"; write-verbose "filename = $($Node.ConfigFile)"; return (Test-Path $Node.ConfigFile); } GetScript = { @{Configured = (Test-Path $Node.ConfigFile)} } DependsOn = "[File]ConfigurationFolder"

            }

    In the Script case, $Node is a null object. All the examples I've seen online for working with configuration data apply to File or Registry resources. Does anyone know how to pass machine-specific values in to a Script resource?

    Thanks,

    Mike

    P.S. I posted the same question to StackOverflow at http://stackoverflow.com/questions/23346901/powershell-dsc-how-to-pass-configuration-parameters-to-scriptresources for anyone else with the same issue.

    Tuesday, April 29, 2014 7:22 AM

Answers

  • Hi David,

    Thanks for this. The major take-home for me is that the SetScript is a *string*. I presume PowerShell must  be converting my script block using ToString when it compiles the Configuration. (That's actually kind of useful because it will syntax check my script block for quotes, escape, etc before turning it into a string).

    Once I realised that, I solved my problem by writing a quick and dirty helper function to beat PowerShell to the punch on the string conversion, and then inject the $Node values using a very naive search and replace.

    function Format-DscScriptBlock()
    {
        param(
            [parameter(Mandatory=$true)]
            [System.Collections.Hashtable] $node,
            [parameter(Mandatory=$true)]
            [System.Management.Automation.ScriptBlock] $scriptBlock
        )
        $result = $scriptBlock.ToString();
        foreach( $key in $node.Keys )
        {
            $source = $result.Replace("`$Node.$key", $node[$key]);
        }
        return $result;
    }

    My SetScript then becomes:

    SetScript = Format-DscScriptBlock -Node $Node -ScriptBlock {
                    write-verbose "running ConfigurationFile.SetScript";
                    write-verbose "folder = $Node.ConfigFolder";
                    write-verbose "filename = $Node.ConfigFile)";
                    [System.IO.File]::WriteAllText("$Node.ConfigFile", "enabled=" + $Node.Enabled);
                }

    It means you have to fudge the configuration values a little bit to help with escaping quotes, etc but some of that could be automatically fixed up in a smarter version of Format-DscScriptBlock.

    Thanks again for your help - it's very much appreciated.

    Mike

    Tuesday, May 06, 2014 7:26 AM

All replies

  • Hi Mike,

    Based on my research, all the build-in configuration resources can be available here:

    Built-In Windows PowerShell Desired State Configuration Resources

    Which contains registry, file and script, you can also get the script example here:

    Windows PowerShell Desired State Configuration Script Resource

    Using PowerShell DSC Script Resource Provider

    A look at the DSC script Resource

    If you have any feedback on our support, please click here.

    Best Regards,

    Anna

    TechNet Community Support

    Wednesday, April 30, 2014 9:11 AM
    Moderator
  • Hi Mike,

    I’m writing to just check in to see if the suggestions were helpful. If you need further help, please feel free to reply this post directly so we will be notified to follow it up.

    If you have any feedback on our support, please click here.

    Best Regards,

    Anna

    TechNet Community Support

    Monday, May 05, 2014 1:45 AM
    Moderator
  • The configuration data only exists at the time the mof file is compiled, not at runtime. However, the script resource's SetScript, TestScript, and GetScript properties are actually strings, not ScriptBlocks.  You can construct the string representation of your script block (with all of the configuration data already expanded) to go into the MOF file. For example, here's how your current script might be written using a here-string:

        Script ConfigurationFile {
            SetScript =
    @"
                write-verbose "running ConfigurationFile.SetScript";
                write-verbose "folder = $($Node.ConfigFolder)";
                write-verbose "filename = $($Node.ConfigFile)";
                [System.IO.File]::WriteAllText('$($Node.ConfigFile)', "enabled=" + '$($Node.Enabled)');
    "@
    
            TestScript =
    @"
                write-verbose "running ConfigurationFile.TestScript";
                write-verbose "folder = $($Node.ConfigFolder)";
                write-verbose "filename = $($Node.ConfigFile)";
                return (Test-Path '$($Node.ConfigFile)');
    "@
    
            GetScript =
    @"
                @{ Configured = (Test-Path '$($Node.ConfigFile)') }
    "@
            DependsOn = "[File]ConfigurationFolder"
        }

    Personally, I find this here-string approach to be kind of a pain.  It's easy to forget to escape something properly or use sub-expressions and quotation marks in all the right ways.  What I would probably do is write a function to generate the script block strings based on parameters passed in.  Then your Script resource would also look much cleaner, something like this:

        Script ConfigurationFile {
            SetScript = Get-MySetScriptBlock -ConfigFolder $Node.ConfigFolder -ConfigFile $Node.ConfigFile
    
            TestScript = Get-MyTestScriptBlock -ConfigFolder $Node.ConfigFolder -ConfigFile $Node.ConfigFile
    
            GetScript = Get-MyGetScriptBlock -ConfigFolder $Node.ConfigFolder -ConfigFile $Node.ConfigFile
    
            DependsOn = "[File]ConfigurationFolder"
        }

    On a side note, the GetScript is next to useless (and won't even work in this example, because the script resource has no "Configured" property.)  Since nothing in normal DSC operation calls the Get-TargetResource function right now anyway, you can probably get away with just returning an empty hashtable with GetScript = { @{} }.



    Monday, May 05, 2014 3:36 AM
  • Hi David,

    Thanks for this. The major take-home for me is that the SetScript is a *string*. I presume PowerShell must  be converting my script block using ToString when it compiles the Configuration. (That's actually kind of useful because it will syntax check my script block for quotes, escape, etc before turning it into a string).

    Once I realised that, I solved my problem by writing a quick and dirty helper function to beat PowerShell to the punch on the string conversion, and then inject the $Node values using a very naive search and replace.

    function Format-DscScriptBlock()
    {
        param(
            [parameter(Mandatory=$true)]
            [System.Collections.Hashtable] $node,
            [parameter(Mandatory=$true)]
            [System.Management.Automation.ScriptBlock] $scriptBlock
        )
        $result = $scriptBlock.ToString();
        foreach( $key in $node.Keys )
        {
            $source = $result.Replace("`$Node.$key", $node[$key]);
        }
        return $result;
    }

    My SetScript then becomes:

    SetScript = Format-DscScriptBlock -Node $Node -ScriptBlock {
                    write-verbose "running ConfigurationFile.SetScript";
                    write-verbose "folder = $Node.ConfigFolder";
                    write-verbose "filename = $Node.ConfigFile)";
                    [System.IO.File]::WriteAllText("$Node.ConfigFile", "enabled=" + $Node.Enabled);
                }

    It means you have to fudge the configuration values a little bit to help with escaping quotes, etc but some of that could be automatically fixed up in a smarter version of Format-DscScriptBlock.

    Thanks again for your help - it's very much appreciated.

    Mike

    Tuesday, May 06, 2014 7:26 AM
  • This helped me too!

    The Format-DSCScriptBlock for loop should read "$result = $result.replace("`$Node.$key",$node[$key]);" else subsequent node keys will overwrite previous replacements and you're returning $result which would be, as is, just the the original script block. 


    Tuesday, May 13, 2014 7:03 PM
  • Ah yes. That'll teach me to cut, paste and then edit code in the forum editor without testing it again afterwards! Glad it was useful anyway.

    M

    Tuesday, May 13, 2014 8:19 PM
  • Hey Michael,

    Thanks for the nifty function. Just wondering if it should be returning the $source variable instead of the $result variable since $soucre is the one defined with the nifty string changes.

    Thanks,

    Ramon Young

    Friday, August 22, 2014 4:14 PM
  • Hi Ramon,

    Someone else mentioned that as well. I updated it at the stackoverflow page where I asked the same question (see http://stackoverflow.com/questions/23346901/powershell-dsc-how-to-pass-configuration-parameters-to-scriptresources/23488957#23488957), but for completeness the corrected code is below as well:

    function Format-DscScriptBlock()
    {
        param(
            [parameter(Mandatory=$true)]
            [System.Collections.Hashtable] $node,
            [parameter(Mandatory=$true)]
            [System.Management.Automation.ScriptBlock] $scriptBlock
        )
        $result = $scriptBlock.ToString();
        foreach( $key in $node.Keys )
        {
            $result = $result.Replace("`$Node.$key", $node[$key]);
        }
        return $result;
    }

    Cheers,

    Mike

    Saturday, August 23, 2014 11:20 AM
  • I've recently learned that if you pass a script block to these properties and use the $using: scope modifier, PowerShell will automatically inject those variables into the script in the MOF document.  For example:

    $configData = @{
        AllNodes = @(
            @{
                NodeName = 'localhost'
                Property = 'My Value'
            }
        )
    }
    
    configuration Test
    {
        Node localhost {
            Script TestScript {
                GetScript = {
                    return @{
                        Result = $true
                    }
                }
    
                SetScript = {
                    Write-Verbose "Property: $($using:Node.Property)"
                }
    
                TestScript = {
                    Write-Verbose ('Property: ' + $using:Node.Property)
                }
            }
        }
    }

    Produces a MOF document with something like this (line breaks injected by me for readability):

    TestScript = "
        $Node = [System.Management.Automation.PSSerializer]::Deserialize(
            '<Objs Version=\"1.1.0.1\" xmlns=\"http://schemas.microsoft.com/powershell/2004/04\">
            \n  <Obj RefId=\"0\">
            \n    <TN RefId=\"0\">
            \n      <T>System.Collections.Hashtable</T>
            \n      <T>System.Object</T>
            \n    </TN>
            \n    <DCT>
            \n      <En>
            \n        <S N=\"Key\">NodeName</S>
            \n        <S N=\"Value\">localhost</S>
            \n      </En>
            \n      <En>
            \n        <S N=\"Key\">Property</S>
            \n        <S N=\"Value\">My Value</S>
            \n      </En>
            \n    </DCT>
            \n  </Obj>
            \n</Objs>')
        \n
        \n                
        Write-Verbose ('Property: ' + $Node.Property)
        \n
    ";

    Sunday, August 24, 2014 2:42 AM
  • Hi David,

    Thanks for the update. I've managed to generate a mof with the "$using" approach and it looks good, although I've not tried applying the mof to a machine yet to see how it works with my config object.

    It's a lot cleaner than my Format-DscScriptBlock function though, and it'll be a lot more robust around escape sequences and special characters in strings so I'll certainly be giving it a go.

    M

    Monday, August 25, 2014 4:13 PM
  • hi,

    You should place a great deal of thought into the usage of the Script resource in DSC.  I see very, very few scenarios where the script resource is a good solution for your configurations. I basically use it for testing. Before I move to production I create a proper DSC resource (doesn't take that long).

    Basically I use a nested Replace on the GetScript/SetScript/Testscript properties.

    TestScript = ([string]{
        Write-verbose "In TestScript TestingSCCM distributionpoint"
        Write-Verbose "Computername is $ComputerName"
        Test-Connection -ComputerName $dummy
        Import-Module "$(Split-Path $env:SMS_ADMIN_UI_PATH -Parent)\ConfigurationManager.psd1" -Verbose:$false
        Write-Verbose "Setting location to CMSite"
        $site = Get-PSDrive | where {$_.Provider.Name -eq "CMSite"}
        Write-Verbose "Site=$($site.Name)"
        cd ("$site" + ":")
        $DP = $null
        $DP = Get-CMDistributionPoint -SiteSystemServerName $ComputerName -ErrorAction SilentlyContinue
        $test = $False
        if($DP)
        {
            $test = $true
        }
        write-verbose "Return $test"
        $test
    }).Replace('$ComputerName',"$ComputerName").Replace('$dummy',"$dummy")

    Have not thought about $using, probably a better idea.

    /Tore

    Friday, September 26, 2014 1:03 PM
  • Hi Mike.

    Great tip. I'm using your Format-DscScriptBlock function around my Script Resources.
    O
    nly note -  because it uses the following line:

    result = $result.Replace("`$Node.$key", $node[$key]);

    it requires that the 'node' referenced in within the scriptblock is uppercase
    e.g $Node.something

    I added a second line to your function to make it case insensitive.

    result = $result.Replace("`$Node.$key", $node[$key]);
    result = $result.Replace("`$node.$key", $node[$key]);

    Great article.

     

    Friday, October 31, 2014 4:09 AM