Hashtable as resource parameter

I’ve been squeezing my brain trying to figure out how to get *-TargetResource to accept a [hashtable] parameter. Can it be done?

#psm1:
param([hashtable]$AdvancedProperties)

#schema:
[Write] String AdvancedProperties;

breaks with

Write-NodeMOFFile : Invalid MOF definition for node 'localhost': Exception calling "ValidateInstanceText" with "1"
argument(s): "Syntax error:
 At line:40, char:48
 Buffer:
 $MSFT_KeyValuePair1ref,^
   $M
"
At
C:\windows\system32\windowspowershell\v1.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psm1:1457
char:17
+ ...             Write-NodeMOFFile $name $mofNode $Script:NodeInstanceAlia ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Write-Error], InvalidOperationException
    + FullyQualifiedErrorId : InvalidMOFDefinition,Write-NodeMOFFile

So that’s no good, obviously.

#psm1:
param([hashtable]$AdvancedProperties)

#schema
[write] Hashtable AdvancedProperties;

is not recognized as a valid schema value at all, and the resource isn’t registered with DSC.

#psm1:
param([hashtable]$AdvancedProperties)

#schema
[write, EmbeddedInstance("MSFT_KeyValuePair")] String AdvancedProperties;

breaks with the following error:

Write-NodeMOFFile : Invalid MOF definition for node 'localhost': Exception calling "ValidateInstanceText" with "1"
argument(s): "Syntax error:
 At line:40, char:48
 Buffer:
 $MSFT_KeyValuePair1ref,^
   $M
"
At
C:\windows\system32\windowspowershell\v1.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psm1:1457
char:17
+ ...             Write-NodeMOFFile $name $mofNode $Script:NodeInstanceAlia ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Write-Error], InvalidOperationException
    + FullyQualifiedErrorId : InvalidMOFDefinition,Write-NodeMOFFile

And finally, I found a PowerShell Team person (http://blogs.msdn.com/b/powershell/archive/2013/11/19/resource-designer-tool-a-walkthrough-writing-a-dsc-resource.aspx) suggesting that it should be set up as such:

#psm1
param([Microsoft.Management.Infrastructure.CimInstance]$AdvancedProperties)

#schema
[write, EmbeddedInstance("MSFT_KeyValuePair")] String AdvancedProperties;

but that also fails with the same error as previously

Write-NodeMOFFile : Invalid MOF definition for node 'localhost': Exception calling "ValidateInstanceText" with "1"
argument(s): "Syntax error:
 At line:25, char:48
 Buffer:
 $MSFT_KeyValuePair1ref,^
   $M
"
At
C:\windows\system32\windowspowershell\v1.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psm1:1457
char:17
+ ...             Write-NodeMOFFile $name $mofNode $Script:NodeInstanceAlia ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Write-Error], InvalidOperationException
    + FullyQualifiedErrorId : InvalidMOFDefinition,Write-NodeMOFFile

It breaks using both WMF4 and WMF5 Preview.

This is the configuration file I’m using:

configuration DSCModuleTest {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ComputerName
    )

    Import-DscResource -ModuleName devNetAdapterModule

    Node $ComputerName {
        sNetAdapterAdvancedProperty Property {
            InterfaceAlias = 'Ethernet'
            AdvancedProperties = @{
                'Jumbo Packet'         = '1514'
                'Receive Side Scaling' = '1'
            }
        }
    }
}

DSCModuleTest -ComputerName 'localhost'

and the associated DSCResource as it looks currently:


######################################################################################
# The Get-TargetResource cmdlet.
# This function will get the working interface and its bindings
######################################################################################
function Get-TargetResource
{
    [CmdletBinding()]
	param
	(		
		[Parameter(Mandatory)]
		[ValidateNotNullOrEmpty()]
        [string]$InterfaceAlias
	)
	
    $properties = GetNetAdapterAdvancedProperty -InterfaceAlias $InterfaceAlias -Formatted
    
    Write-Output $properties
}

######################################################################################
# The Set-TargetResource cmdlet.
# This function will set the bindings of the selected interface
######################################################################################
function Set-TargetResource
{
    [CmdletBinding()]
	param
	(
		[Parameter(Mandatory)]
		[ValidateNotNullOrEmpty()]
        [string]$InterfaceAlias,

        [Parameter(Mandatory)]
        [hashtable]$AdvancedProperties
        #[Microsoft.Management.Infrastructure.CimInstance]$AdvancedProperties
	)

    $properties = GetNetAdapterAdvancedProperty -InterfaceAlias $InterfaceAlias

    foreach($key in $AdvancedProperties.Keys) {
        Write-Verbose ('Processing property {0}' -f $key)
        
        $property = $properties | Where-Object DisplayName -eq $key
        if($property -eq $null) {
            throw "Property $key not found. Use the Get-NetAdapterAdvancedProperty cmdlet to see available properties for your interface, and configure DSC AdvancedProperties hashtable to use 'DisplayName' property as key, and 'RegistryValue' as value."
        }
        
        Write-Verbose ('Found and now setting property {0}' -f $property.DisplayName)
        $property | Set-NetAdapterAdvancedProperty -RegistryValue $AdvancedProperties.$key
    }
}

######################################################################################
# The Test-TargetResource cmdlet.
# This will test if the interface bindings are as expected
######################################################################################
function Test-TargetResource
{
    [CmdletBinding()]
    param
	(		
		[Parameter(Mandatory)]
		[ValidateNotNullOrEmpty()]
		[string]$InterfaceAlias,
        
        [Parameter(Mandatory)]
        [hashtable]$AdvancedProperties
        #[Microsoft.Management.Infrastructure.CimInstance]$AdvancedProperties
	)

    $properties = GetNetAdapterAdvancedProperty -InterfaceAlias $InterfaceAlias -Formatted

    foreach($key in $AdvancedProperties.Keys) {
        Write-Verbose ('Processing property {0}' -f $key)
        
        $property = $properties | Where-Object DisplayName -eq $key
        if($property -eq $null) {
            throw "Property $key not found. Use the Get-NetAdapterAdvancedProperty cmdlet to see available properties for your interface, and configure DSC AdvancedProperties hashtable to use 'DisplayName' property as key, and 'RegistryValue' as value."
        }
        
        Write-Verbose ('Found and now testing property {0}' -f $property.DisplayName)
        if($property.RegistryValue -ne $AdvancedProperties.$key) {
            Write-Verbose ('Property "{0}" with expected value "{1}" does not match set value "{2}"' -f $key, $AdvancedProperties.$key, $property.RegistryValue)
            return $false
        }
    }

    return $true
}


#######################################################################################
#  Helper function that validates the IP Address properties. If the switch parameter
# "Apply" is set, then it will set the properties after a test
#######################################################################################
function GetNetAdapterByAlias {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$InterfaceAlias
    )

    Write-Verbose "Attempting to find NetAdapter using Alias $InterfaceAlias"
    $adapter = Get-NetAdapter -InterfaceAlias $InterfaceAlias

    if($adapter -ne $null -and @($adapter).Count -eq 1) {
        Write-Verbose ('Found {0}' -f $adapter.Name)
        return $adapter
    }
    else {
        if(@($adapter).Count -gt 1) { 
            throw ("Too many adapters found for Alias $InterfaceAlias ({0})" -f ($adapter.Name -join ', '))
        }
        elseif($adapter -eq $null) { 
            throw "No adapter found for Alias $InterfaceAlias"
        }
        else {
            throw ('Unexpected error? Adapter count: {0}. InterfaceAlias: {1}.' -f @($adapter).Count, $InterfaceAlias)
        }
    }
}

function GetNetAdapterAdvancedProperty {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$InterfaceAlias,

        [switch]$Formatted
    )

    $adapter = GetNetAdapterByAlias -InterfaceAlias $InterfaceAlias

    Write-Verbose "Getting advanced properties"
    $properties = $adapter | Get-NetAdapterAdvancedProperty
    if($properties -eq $null) {
        throw ("Unable to acquire bindings for NetAdapter {0}" -f $adapter.Name)
    }

    if($Formatted) { 
        Write-Verbose "Formatting properties"
        $returnValue = foreach($property in $properties) {
            Write-Output @{
                $property.DisplayName = $property.DisplayValue
            }
        }

	    Write-Output $returnValue
    }
    else {
        Write-Output $properties
    }
}


#  FUNCTIONS TO BE EXPORTED 
Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource

Oh, and running it with -Debug gives the following:

DEBUG:    SHT_sNetAdapterAdvancedProperty: RESOURCE PROCESSING STARTED [KeywordName='sNetAdapterAdvancedProperty'] Function='devNetAdapterModule\sNetAdapterAdvancedProperty']
DEBUG:    SHT_sNetAdapterAdvancedProperty: ResourceID = [sNetAdapterAdvancedProperty]Property
DEBUG:    SHT_sNetAdapterAdvancedProperty:  Processing property 'DependsOn' [
DEBUG:    SHT_sNetAdapterAdvancedProperty:        Canonicalized property 'DependsOn' = ''
DEBUG:    SHT_sNetAdapterAdvancedProperty:    Processing completed 'DependsOn' ]
DEBUG:    SHT_sNetAdapterAdvancedProperty:  Processing property 'InterfaceAlias' [
DEBUG:    SHT_sNetAdapterAdvancedProperty:        Canonicalized property 'InterfaceAlias' = 'Ethernet'
DEBUG:    SHT_sNetAdapterAdvancedProperty:    Processing completed 'InterfaceAlias' ]
DEBUG:    SHT_sNetAdapterAdvancedProperty:  Processing property 'AdvancedProperties' [
DEBUG:    SHT_sNetAdapterAdvancedProperty:        Canonicalized property 'AdvancedProperties' = 'System.Collections.Hashtable'
DEBUG:    SHT_sNetAdapterAdvancedProperty:    Processing completed 'AdvancedProperties' ]
DEBUG:    SHT_sNetAdapterAdvancedProperty: MOF alias for this resource is '$SHT_sNetAdapterAdvancedProperty1ref'
DEBUG:    SHT_sNetAdapterAdvancedProperty: RESOURCE PROCESSING COMPLETED. TOTAL ERROR COUNT: 0
Write-NodeMOFFile : Invalid MOF definition for node 'localhost': Exception calling "ValidateInstanceText" with "1" argument(s): "Syntax error: 
 At line:25, char:48
 Buffer:
 $MSFT_KeyValuePair1ref,^
   $M
"
At C:\Windows\system32\WindowsPowerShell\v1.0\Modules\PSDesiredStateConfiguration\PSDesiredStateConfiguration.psm1:1425 char:17
+                 Write-NodeMOFFile $name $mofNode $Script:NodeInstanceAliases[$mo ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Write-Error], InvalidOperationException
    + FullyQualifiedErrorId : InvalidMOFDefinition,Write-NodeMOFFile

and the mof.error file that’s generated:

/*
@TargetNode='localhost'
@GeneratedBy=mni
@GenerationDate=08/13/2014 11:08:57
@GenerationHost=MNI-PC
*/

instance of MSFT_KeyValuePair as $MSFT_KeyValuePair1ref
{
Key = "Receive Side Scaling";
 Value = "1";

};

instance of MSFT_KeyValuePair as $MSFT_KeyValuePair2ref
{
Key = "Jumbo Packet";
 Value = "1514";

};

instance of SHT_sNetAdapterAdvancedProperty as $SHT_sNetAdapterAdvancedProperty1ref
{
ResourceID = "[sNetAdapterAdvancedProperty]Property";
 AdvancedProperties =    $MSFT_KeyValuePair1ref,
   $MSFT_KeyValuePair2ref
;
 SourceInfo = "C:\\DSC\\dsc.ps1::10::9::sNetAdapterAdvancedProperty";
 ModuleName = "devNetAdapterModule";
 InterfaceAlias = "Ethernet";
 ModuleVersion = "1.1";

};

instance of OMI_ConfigurationDocument
{
 Version="1.0.0";
 Author="mni";
 GenerationDate="08/13/2014 11:08:57";
 GenerationHost="MNI-PC";
};

Found something…

Setting the schema property to

[Write, EmbeddedInstance(“MSFT_KeyValuePair”)] String AdvancedProperties;
lets DSC read and generate a proper mof file if you can believe it.

Only thing is now I’m getting

PowerShell DSC resource SHT_sNetAdapterAdvancedProperty  failed to execute Test-TargetResource functionality with
error message: Cannot process argument transformation on parameter 'AdvancedProperties'. Cannot convert the
"Microsoft.Management.Infrastructure.CimInstance[]" value of type "Microsoft.Management.Infrastructure.CimInstance[]"
to type "System.Collections.Generic.KeyValuePair`2[System.String,System.String]".
    + CategoryInfo          : InvalidOperation: (:) [], CimException
    + FullyQualifiedErrorId : ProviderOperationExecutionFailure
    + PSComputerName        : localhost

if I set the param to either no type or KeyValuePair[[string],[string]].

If I set the param to [hashtable], [Microsoft.Management.Infrastructure.CimInstance] or [Microsoft.Management.Infrastructure.CimInstance] I get

PowerShell DSC resource SHT_sNetAdapterAdvancedProperty  failed to execute Test-TargetResource functionality with
error message: Cannot process argument transformation on parameter 'AdvancedProperties'. Cannot convert the
"Microsoft.Management.Infrastructure.CimInstance[]" value of type "Microsoft.Management.Infrastructure.CimInstance[]"
to type "System.Collections.Hashtable".
    + CategoryInfo          : InvalidOperation: (:) [], CimException
    + FullyQualifiedErrorId : ProviderOperationExecutionFailure
    + PSComputerName        : localhost

At least I’m getting somewhere though. I think.

Aha, success!

#psm1
param(
    [Microsoft.Management.Infrastructure.CimInstance[]]$AdvancedProperties
)

foreach($instance in $AdvancedProperties) {
    Write-Verbose ('Key: {0}, Value: {1}' -f $instance.Key, $instance.Value)
}

#schema
[Write, EmbeddedInstance("MSFT_KeyValuePair")] String AdvancedProperties[];

This actually works. Fantastic. It may be that it takes a Hashtable in the DSC configuration, but trying to access Hashtable properties like .Keys apparently makes PowerShell attempt an implicit conversion from KeyValuePair to Hashtable, and this breaks quite spectacularly.

Now to somehow make Get-TargetResource to work…

I wouldn’t stress too much about Get-TargetResource at this point. As far as I know, nothing ever calls it (unless you use it internally as part of the resource’s Test or Set functions). Until the DSC engine begins to rely on Get-TargetResource for something, you’d just be making guesses at what the output hashtable should look like when dealing with these embedded instances. (I suspect that it should contain arrays of MSFT_KeyValuePair objects, just as they’re passed to Set-TargetResource, but who knows?)

I had a chance to test out an idea using the same principle you referred to in the other thread, and it was a great success.

#schema
{
  [key] String KeyVariable;
  [write, EmbeddedInstance("MSFT_KeyValuePair")] String AdvancedResource;
};

#Get-TargetResource
return @{
        KeyVariable = $KeyVariable
        AdvancedResource = New-CimInstance -ClassName MSFT_KeyValuePair -Namespace root/microsoft/Windows/DesiredStateConfiguration -Property @{
            Key = 'Some key'
            Value = 'Some value'
        } -ClientOnly
    }

#
PS C:\DSC> $conf = Get-DscConfiguration
PS C:\DSC> $conf

AdvancedResource                        KeyVariable                             PSComputerName
--                                      --                                      --
MSFT_KeyValuePair (key = "Some key")    key variable here


PS C:\DSC> $conf.AdvancedResource

key                                     Value                                   PSComputerName
--                                      --                                      --
Some key                                Some value