diff --git a/wmi-adapter/Tests/wmi.tests.ps1 b/wmi-adapter/Tests/wmi.tests.ps1 index e5d2115c..7c479560 100644 --- a/wmi-adapter/Tests/wmi.tests.ps1 +++ b/wmi-adapter/Tests/wmi.tests.ps1 @@ -6,7 +6,7 @@ Describe 'WMI adapter resource tests' { BeforeAll { if ($IsWindows) { - $OldPSModulePath = $env:PSModulePath + $OldPSModulePath = $env:PSModulePath $env:PSModulePath += ";" + $PSScriptRoot $configPath = Join-path $PSScriptRoot "test_wmi_config.dsc.yaml" @@ -19,39 +19,92 @@ Describe 'WMI adapter resource tests' { } } - It 'List shows WMI resources' -Skip:(!$IsWindows){ + Context 'List WMI resources' { + It 'List shows WMI resources' -Skip:(!$IsWindows) { - $r = dsc resource list *OperatingSystem* -a Microsoft.Windows/WMI - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.Count | Should -BeGreaterOrEqual 1 + $r = dsc resource list *OperatingSystem* -a Microsoft.Windows/WMI + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.Count | Should -BeGreaterOrEqual 1 + } } - It 'Get works on an individual WMI resource' -Skip:(!$IsWindows){ + Context 'Get WMI resources' { + It 'Get works on an individual WMI resource' -Skip:(!$IsWindows) { - $r = dsc resource get -r root.cimv2/Win32_OperatingSystem - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.CreationClassName | Should -Be "Win32_OperatingSystem" + $r = dsc resource get -r root.cimv2/Win32_OperatingSystem + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.result.type | Should -BeLike "*Win32_OperatingSystem" + } + + It 'Get works on a config with WMI resources' -Skip:(!$IsWindows) { + + $r = Get-Content -Raw $configPath | dsc config get + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.results.result.actualstate.result[0].properties.LastBootUpTime | Should -Not -BeNull + $res.results.result.actualstate.result[0].properties.Caption | Should -Not -BeNull + $res.results.result.actualstate.result[0].properties.NumberOfProcesses | Should -Not -BeNull + } + + It 'Example config works' -Skip:(!$IsWindows) { + $configPath = Join-Path $PSScriptRoot '..\..\dsc\examples\wmi.dsc.yaml' + $r = dsc config get -p $configPath + $LASTEXITCODE | Should -Be 0 + $r | Should -Not -BeNullOrEmpty + $res = $r | ConvertFrom-Json + $res.results.result.actualstate.result[0].properties.Model | Should -Not -BeNullOrEmpty + $res.results.result.actualstate.result[0].properties.Description | Should -Not -BeNullOrEmpty + } } - It 'Get works on a config with WMI resources' -Skip:(!$IsWindows){ + # TODO: work on set test configs + Context "Set WMI resources" { + It 'Set a resource' -Skip:(!$IsWindows) { + $inputs = @{ + adapted_dsc_type = "root.cimv2/Win32_Process" + properties = @{ + MethodName = 'Create' + CommandLine = 'powershell.exe' + } + } + # get the start of processes + $ref = Get-Process + + # run the creation of process + $r = ($inputs | ConvertTo-Json -Compress) | dsc resource set -r root.cimv2/Win32_Process - $r = Get-Content -Raw $configPath | dsc config get - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.results[0].result.actualState[0].LastBootUpTime | Should -Not -BeNull - $res.results[0].result.actualState[1].BiosCharacteristics | Should -Not -BeNull - $res.results[0].result.actualState[2].NumberOfLogicalProcessors | Should -Not -BeNull + # handle the output as we do not have a filter yet on the get method + $diff = Get-Process + + $comparison = (Compare-Object -ReferenceObject $ref -DifferenceObject $diff | Where-Object { $_.SideIndicator -eq '=>' }) + $process = foreach ($c in $comparison) + { + if ($c.InputObject.Path -like "*$($inputs.properties.CommandLine)*") + { + $c.InputObject + } + } + $res = $r | ConvertFrom-Json + $res.afterState.result | Should -Not -BeNull + $LASTEXITCODE | Should -Be 0 + $process | Should -Not -BeNullOrEmpty + $process.Path | Should -BeLike "*powershell.exe*" + } + AfterAll { + $process = Get-Process -Name "powershell" -ErrorAction SilentlyContinue | Sort-Object StartTime -Descending -Top 1 + Stop-Process $process + } } - It 'Example config works' -Skip:(!$IsWindows) { - $configPath = Join-Path $PSScriptRoot '..\..\dsc\examples\wmi.dsc.yaml' - $r = dsc config get -p $configPath - $LASTEXITCODE | Should -Be 0 - $r | Should -Not -BeNullOrEmpty - $res = $r | ConvertFrom-Json - $res.results[0].result.actualState[0].Model | Should -Not -BeNullOrEmpty - $res.results[0].result.actualState[1].Description | Should -Not -BeNullOrEmpty + Context "Export WMI resources" { + It 'Exports all resources' -Skip:(!$IsWindows) { + $r = dsc resource export -r root.cimv2/Win32_Process + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.resources.properties.result.properties.value.count | Should -BeGreaterThan 1 + $res.resources.properties.result.properties.value[0].CreationClassName | Should -Be 'Win32_Process' + } } } diff --git a/wmi-adapter/copy_files.txt b/wmi-adapter/copy_files.txt index a6bfcb39..a96936dd 100644 --- a/wmi-adapter/copy_files.txt +++ b/wmi-adapter/copy_files.txt @@ -1,2 +1,4 @@ wmi.resource.ps1 -wmi.dsc.resource.json \ No newline at end of file +wmi.dsc.resource.json +wmiAdapter.psd1 +wmiAdapter.psm1 \ No newline at end of file diff --git a/wmi-adapter/wmi.dsc.resource.json b/wmi-adapter/wmi.dsc.resource.json index 5b63c7d3..0d1bb66a 100644 --- a/wmi-adapter/wmi.dsc.resource.json +++ b/wmi-adapter/wmi.dsc.resource.json @@ -1,48 +1,68 @@ { - "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", - "type": "Microsoft.Windows/WMI", - "version": "0.1.0", - "kind": "Adapter", - "description": "Resource adapter to WMI resources.", - "tags": [ - "PowerShell" - ], - "adapter": { - "list": { - "executable": "powershell", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-Command", - "./wmi.resource.ps1 List" - ] - }, - "config": "full" - }, - "get": { + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "type": "Microsoft.Windows/WMI", + "version": "0.1.0", + "kind": "Adapter", + "description": "Resource adapter to WMI resources.", + "tags": ["PowerShell"], + "adapter": { + "list": { "executable": "powershell", "args": [ "-NoLogo", "-NonInteractive", "-NoProfile", "-Command", - "$Input | ./wmi.resource.ps1 Get" - ], - "input": "stdin" + "./wmi.resource.ps1 List" + ] }, - "validate": { - "executable": "powershell", - "args": [ - "-NoLogo", - "-NonInteractive", - "-NoProfile", - "-Command", - "$Input | ./wmi.resource.ps1 Validate" - ] - }, - "exitCodes": { - "0": "Success", - "1": "Error" - } + "config": "full" + }, + "get": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./wmi.resource.ps1 Get" + ], + "input": "stdin" + }, + "set": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./wmi.resource.ps1 Set" + ], + "input": "stdin" + }, + "export": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./wmi.resource.ps1 Export" + ], + "input": "stdin" + }, + "validate": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./wmi.resource.ps1 Validate" + ] + }, + "exitCodes": { + "0": "Success", + "1": "Error" } +} diff --git a/wmi-adapter/wmi.resource.ps1 b/wmi-adapter/wmi.resource.ps1 index 6eb2f88d..504b6277 100644 --- a/wmi-adapter/wmi.resource.ps1 +++ b/wmi-adapter/wmi.resource.ps1 @@ -3,142 +3,147 @@ [CmdletBinding()] param( - [ValidateSet('List','Get','Set','Test','Validate')] - $Operation = 'List', - [Parameter(ValueFromPipeline)] - $stdinput + [Parameter(Mandatory = $true, Position = 0, HelpMessage = 'Operation to perform. Choose from List, Get, Set, Test, Export, Validate.')] + [ValidateSet('List', 'Get', 'Set', 'Test', 'Export', 'Validate')] + [string]$Operation, + [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] + [string]$jsonInput = '@{}' ) -$ProgressPreference = 'Ignore' -$WarningPreference = 'Ignore' -$VerbosePreference = 'Ignore' +function Write-DscTrace +{ + param + ( + [Parameter(Mandatory = $false)] + [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] + [string]$Operation = 'Debug', + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Message + ) + + $trace = @{$Operation = $Message } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) +} -function IsConfiguration($obj) { - if ($null -ne $obj.metadata -and $null -ne $obj.metadata.'Microsoft.DSC' -and $obj.metadata.'Microsoft.DSC'.context -eq 'Configuration') { - return $true - } +# Adding some debug info to STDERR +'PSVersion=' + $PSVersionTable.PSVersion.ToString() | Write-DscTrace +'PSPath=' + $PSHome | Write-DscTrace +'PSModulePath=' + $env:PSModulePath | Write-DscTrace - return $false +if ('Validate' -ne $Operation) +{ + # write $jsonInput to STDERR for debugging + $trace = @{'Debug' = 'jsonInput=' + $jsonInput } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + $wmiAdapter = Import-Module "$PSScriptRoot/wmiAdapter.psd1" -Force -PassThru + + # initialize OUTPUT as array + $result = [System.Collections.Generic.List[Object]]::new() } -if ($Operation -eq 'List') +switch ($Operation) { - $clases = Get-CimClass - - foreach ($r in $clases) + 'List' { - $version_string = ""; - $author_string = ""; - $moduleName = ""; + $clases = Get-CimClass - $propertyList = @() - foreach ($p in $r.CimClassProperties) + foreach ($r in $clases) { - if ($p.Name) + $version_string = ""; + $author_string = ""; + $moduleName = ""; + + $propertyList = @() + foreach ($p in $r.CimClassProperties) { - $propertyList += $p.Name + if ($p.Name) + { + $propertyList += $p.Name + } } - } - - $namespace = $r.CimSystemProperties.Namespace.ToLower().Replace('/','.') - $classname = $r.CimSystemProperties.ClassName - $fullResourceTypeName = "$namespace/$classname" - $requiresString = "Microsoft.Windows/WMI" - - $z = [pscustomobject]@{ - type = $fullResourceTypeName; - kind = 'Resource'; - version = $version_string; - capabilities = @('Get'); - path = ""; - directory = ""; - implementedAs = ""; - author = $author_string; - properties = $propertyList; - requireAdapter = $requiresString - } - - $z | ConvertTo-Json -Compress - } -} -elseif ($Operation -eq 'Get') -{ - $inputobj_pscustomobj = $null - if ($stdinput) - { - $inputobj_pscustomobj = $stdinput | ConvertFrom-Json - } - - $result = @() - - if (IsConfiguration $inputobj_pscustomobj) # we are processing a config batch - { - foreach($r in $inputobj_pscustomobj.resources) - { - $type_fields = $r.type -split "/" - $wmi_namespace = $type_fields[0].Replace('.','\') - $wmi_classname = $type_fields[1] - - #TODO: add filtering based on supplied properties of $r - $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname - - if ($wmi_instances) + + # TODO: create class + $methodList = [System.Collections.Generic.List[PSObject]]@() + foreach ($m in $r.CimClassMethods) { - $instance_result = @{} - $wmi_instance = $wmi_instances[0] # for 'Get' we return just first matching instance; for 'export' we return all instances - $wmi_instance.psobject.properties | %{ - if (($_.Name -ne "type") -and (-not $_.Name.StartsWith("Cim"))) - { - $instance_result[$_.Name] = $_.Value - } + $inputObject = [PSCustomObject]@{ + methodName = $m.Name + parameters = @() } - - $result += @($instance_result) + + if ($m.Parameters) + { + $inputObject.parameters = $m.Parameters.Name + } + $methodList += $inputObject } - else - { - $errmsg = "Can not find type " + $r.type + "; please ensure that Get-CimInstance returns this resource type" - Write-Error $errmsg - exit 1 + + $namespace = $r.CimSystemProperties.Namespace.ToLower().Replace('/', '.') + $classname = $r.CimSystemProperties.ClassName + $fullResourceTypeName = "$namespace/$classname" + $requiresString = "Microsoft.Windows/WMI" + + $z = [pscustomobject]@{ + type = $fullResourceTypeName; + kind = 'Resource'; + version = $version_string; + capabilities = @('Get', 'Set', 'Test', 'Export'); + # capabilities = $methodList + path = ""; + directory = ""; + implementedAs = ""; + author = $author_string; + properties = $propertyList; + # TODO: Could not use methodsDetails because expected one of `type`, `kind`, `version`, `capabilities`, `path`, `description`, `directory`, `implementedAs`, `author`, `properties`, `requireAdapter`, `manifest` + # Where is this coming from? + # methodsDetails = $methodList + requireAdapter = $requiresString } + + $z | ConvertTo-Json -Compress -Depth 10 } } - else # we are processing an individual resource call + { @('Get', 'Set', 'Test', 'Export') -contains $_ } { - $type_fields = $inputobj_pscustomobj.adapted_dsc_type -split "/" - $wmi_namespace = $type_fields[0].Replace('.','\') - $wmi_classname = $type_fields[1] - - #TODO: add filtering based on supplied properties of $inputobj_pscustomobj - $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname - - if ($wmi_instances) + + $desiredState = $wmiAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) + if ($null -eq $desiredState) { - $wmi_instance = $wmi_instances[0] # for 'Get' we return just first matching instance; for 'export' we return all instances - $result = @{} - $wmi_instance.psobject.properties | %{ - if (($_.Name -ne "type") -and (-not $_.Name.StartsWith("Cim"))) - { - $result[$_.Name] = $_.Value - } - } + $trace = @{'Debug' = 'ERROR: Failed to create configuration object from provided input JSON.' } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 } - else + + foreach ($ds in $desiredState) { - $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-CimInstance returns this resource type" - Write-Error $errmsg - exit 1 + # process the INPUT (desiredState) for each resource as dscresourceInfo and return the OUTPUT as actualState + $actualstate = $wmiAdapter.Invoke( { param($op, $ds) Invoke-DscWmi -Operation $op -DesiredState $ds }, $Operation, $ds) + if ($null -eq $actualState) + { + $trace = @{'Debug' = 'ERROR: Incomplete GET for resource ' + $ds.type } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + exit 1 + } + + $result += $actualstate } - } - $result | ConvertTo-Json -Compress -} -elseif ($Operation -eq 'Validate') -{ - # TODO: this is placeholder - @{ valid = $true } | ConvertTo-Json -} -else -{ - Write-Error "ERROR: Unsupported operation requested from wmigroup.resource.ps1" + # OUTPUT json to stderr for debug, and to stdout + $result = @{ result = $result } | ConvertTo-Json -Depth 10 -Compress + $trace = @{'Debug' = 'jsonOutput=' + $result } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) + return $result + } + 'Validate' + { + # VALIDATE not implemented + + # OUTPUT + @{ valid = $true } | ConvertTo-Json + } + Default + { + Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' + } } \ No newline at end of file diff --git a/wmi-adapter/wmiAdapter.psd1 b/wmi-adapter/wmiAdapter.psd1 new file mode 100644 index 00000000..4dd03d3d --- /dev/null +++ b/wmi-adapter/wmiAdapter.psd1 @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'wmiAdapter.psm1' + + # Version number of this module. + moduleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = '420c66dc-d243-4bf8-8de0-66467328f4b7' + + # Author of this module + Author = 'Microsoft Corporation' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell Desired State Configuration Module for DSC WMI Adapter' + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Invoke-DscWmi' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + PrivateData = @{ + PSData = @{ + ProjectUri = 'https://github.com/PowerShell/dsc' + } + } +} + \ No newline at end of file diff --git a/wmi-adapter/wmiAdapter.psm1 b/wmi-adapter/wmiAdapter.psm1 new file mode 100644 index 00000000..2498b01a --- /dev/null +++ b/wmi-adapter/wmiAdapter.psm1 @@ -0,0 +1,391 @@ +function Write-DscTrace +{ + param( + [Parameter(Mandatory = $false)] + [ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')] + [string]$Operation = 'Debug', + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string]$Message + ) + + $trace = @{$Operation = $Message } | ConvertTo-Json -Compress + $host.ui.WriteErrorLine($trace) +} + +function Get-DscResourceObject +{ + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + $jsonInput + ) + # normalize the INPUT object to an array of dscResourceObject objects + $inputObj = $jsonInput | ConvertFrom-Json -ErrorAction SilentlyContinue + $desiredState = [System.Collections.Generic.List[Object]]::new() + + # catch potential for improperly formatted configuration input + if ($inputObj.resources -and -not $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') + { + $msg = 'The input has a top level property named "resources" but is not a configuration. If the input should be a configuration, include the property: "metadata": {"Microsoft.DSC": {"context": "Configuration"}}' + $msg | Write-DscTrace -Operation Warn + } + + $adapterName = 'Microsoft.Windows/WMI' + + if ($null -ne $inputObj.metadata -and $null -ne $inputObj.metadata.'Microsoft.DSC' -and $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') + { + # change the type from pscustomobject to dscResourceObject + $inputObj.resources | ForEach-Object -Process { + $desiredState += [dscResourceObject]@{ + name = $_.name + type = $_.type + properties = $_.properties + } + } + } + else + { + # mimic a config object with a single resource + $type = $inputObj.adapted_dsc_type + if (-not $type) + { + $errmsg = "Can not find " + $jsonInput + ". Please make sure the payload contains the 'adapted_dsc_type' key property." + $errmsg | Write-DscTrace -Operation Error + exit 1 + } + + $inputObj.psobject.properties.Remove('adapted_dsc_type') + $desiredState += [dscResourceObject]@{ + name = $adapterName + type = $type + properties = $inputObj.properties + } + } + return $desiredState +} + +function GetCimSpace +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateSet('Get', 'Set', 'Test', 'Export')] + [System.String] + $Operation, + + [Parameter(Mandatory, ValueFromPipeline = $true)] + [psobject] + $DesiredState + ) + + $addToActualState = [dscResourceObject]@{} + $DesiredState.psobject.properties | ForEach-Object -Process { + if ($_.TypeNameOfValue -EQ 'System.String') { $addToActualState.$($_.Name) = $DesiredState.($_.Name) } + } + + foreach ($r in $DesiredState) + { + $type_fields = $r.type -split "/" + $wmi_namespace = $type_fields[0].Replace('.', '\') + $wmi_classname = $type_fields[1] + + #TODO: add filtering based on supplied properties of $r + $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname + + if ($wmi_instances) + { + $instance_result = @{} + switch ($Operation) + { + 'Get' + { + $instance_result = @{} + $wmi_instance = $wmi_instances[0] # for 'Get' we return just first matching instance; for 'export' we return all instances + $wmi_instance.psobject.properties | ForEach-Object { + if (($_.Name -ne "type") -and (-not $_.Name.StartsWith("Cim"))) + { + $instance_result[$_.Name] = $_.Value + } + } + + $addToActualState.properties = $instance_result + + # TODO: validate if we can set it to null + $addToActualState.CimInstance = $null + } + 'Set' + { + # TODO: with the wmi_instances now added on top, it becomes easier to apply some logic on the parameters available to Get-CimInstance + $wmi_instance = $wmi_instances[0] + + # add the properties from INPUT + $addToActualState.properties = $r.properties + + # return the Microsoft.Management.Infrastructure.CimInstance class + $addToActualState.CimInstance = $wmi_instance + + } + 'Test' + { + # TODO: implement test + } + 'Export' + { + foreach ($wmi_instance in $wmi_instances) + { + $wmi_instance.psobject.properties | ForEach-Object { + if (($_.Name -ne "type") -and (-not $_.Name.StartsWith("Cim"))) + { + $instance_result[$_.Name] = $_.Value + } + } + + $addToActualState.properties += @($instance_result) + } + } + } + + return $addToActualState + } + else + { + $errmsg = "Can not find type " + $addToActualState.type + "; please ensure that Get-CimInstance returns this resource type" + $errmsg | Write-DscTrace -Operation Error + exit 1 + } + } +} + +function ValidateCimMethodAndArguments +{ + # TODO: whenever dsc exit codes come in add them, see: https://github.com/PowerShell/DSC/issues/421 + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [dscResourceObject[]] + $DesiredState + + ) + + $inputObject = [System.Collections.Generic.List[hashtable]]@{} + + foreach ($r in $DesiredState) + { + $methodName = $r.properties.MethodName + if (-not $methodName) + { + $errmsg = 'Can not find method name when calling ' + $DesiredState.type + '; Please add "MethodName" in input.' + 'ERROR: ' + $errmsg | Write-DscTrace + exit 1 + } + + $className = $r.type.Split("/")[-1] + $namespace = $r.type.Split("/")[0].Replace(".", "/") + $class = Get-CimClass -ClassName $className -Namespace $namespace + + $classMethods = $class.CimClassMethods.Name + if ($classMethods -notcontains $methodName) + { + $errmsg = 'Method ' + ('"{0}"' -f $r.properties.MethodName) + ' was not found on ' + $r.type + "; Please ensure you call the correct method" + # $debugmsg = 'Available method(s) ' + ('{0}' -f ($class.CimClassMethods.Name | ConvertTo-Json -Compress)) + 'ERROR: ' + $errmsg | Write-DscTrace + #'DEBUG: ' + $debugmsg | Write-DscTrace + exit 1 + } + + $parameters = $class.CimClassMethods.parameters.Name + $props = $r.properties | Get-Member | Where-Object { $_.MemberType -eq 'NoteProperty' } | Select-Object -ExpandProperty Name + + # TODO: can also validate if empty values are provided and which might be mandatory + $arguments = @{} + if (-not ($null -eq $props)) + { + $props | ForEach-Object { + $propertyName = $_ + if ($propertyName -notin $parameters) + { + $msg = 'Parameter ' + $propertyName + " not found on $className." + 'WARNING: ' + $msg | Write-DscTrace + } + else + { + $arguments += @{$propertyName = $r.Properties.$propertyName } + } + } + } + + # return hash table of parameters for InvokeCimMethod + $inputObject += @{ + CimInstance = $r.CimInstance + MethodName = $methodName + Arguments = $arguments + } + } + + return $inputObject +} + +function Invoke-DscWmi +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateSet('Get', 'Set', 'Test', 'Export')] + [System.String] + $Operation, + + [Parameter(Mandatory, ValueFromPipeline = $true)] + [dscResourceObject] + $DesiredState + ) + + $osVersion = [System.Environment]::OSVersion.VersionString + 'OS version: ' + $osVersion | Write-DscTrace + + $psVersion = $PSVersionTable.PSVersion.ToString() + 'PowerShell version: ' + $psVersion | Write-DscTrace + + switch ($Operation) + { + 'Get' + { + $addToActualState = GetCimSpace -Operation $Operation -DesiredState $DesiredState + } + 'Set' + { + $addToActualState = GetCimSpace -Operation $Operation -DesiredState $DesiredState + + $wmiResources = ValidateCimMethodAndArguments -DesiredState $addToActualState + foreach ($resource in $wmiResources) + { + $null = InvokeCimMethod @resource + } + + # reset the value to be empty + $addToActualState = [PSCustomObject]@{ + name = $addToActualState.name + type = $addToActualState.type + properties = $null + } + } + 'Test' + { + + } + 'Export' + { + $addToActualState = GetCimSpace -Operation $Operation -DesiredState $DesiredState + } + } + + return $addToActualState +} + + +function InvokeCimMethod +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimMethodResult])] + param + ( + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance] + $CimInstance, + + [Parameter(Mandatory = $true)] + [System.String] + $MethodName, + + [Parameter()] + [System.Collections.Hashtable] + $Arguments + ) + + $invokeCimMethodParameters = @{ + MethodName = $MethodName + ErrorAction = 'Stop' + } + + if ($PSBoundParameters.ContainsKey('Arguments')) + { + $invokeCimMethodParameters['Arguments'] = $Arguments + } + + try + { + $invokeCimMethodResult = $CimInstance | Invoke-CimMethod @invokeCimMethodParameters + } + catch [Microsoft.Management.Infrastructure.CimException] + { + $errMsg = $_.Exception.Message.Trim("") + if ($errMsg -eq 'Invalid method') + { + "Retrying without instance" | Write-DscTrace -Operation Trace + $invokeCimMethodResult = Invoke-CimMethod @invokeCimMethodParameters -ClassName $CimInstance[0].CimClass.CimClassName + } + } + catch + { + $errmsg = "Could not execute 'Invoke-CimMethod' with error message: " + $_.Exception.Message + 'ERROR: ' + $errmsg | Write-DscTrace + exit 1 + } + + <# + Successfully calling the method returns $invokeCimMethodResult.HRESULT -eq 0. + If an general error occur in the Invoke-CimMethod, like calling a method + that does not exist, returns $null in $invokeCimMethodResult. + #> + if ($invokeCimMethodResult.HRESULT) + { + $res = $invokeCimMethodResult.HRESULT + } + else + { + $res = $invokeCimMethodResult.ReturnValue + } + if ($invokeCimMethodResult -and $res -ne 0) + { + if ($invokeCimMethodResult | Get-Member -Name 'ExtendedErrors') + { + <# + The returned object property ExtendedErrors is an array + so that needs to be concatenated. + #> + $errorMessage = $invokeCimMethodResult.ExtendedErrors -join ';' + } + else + { + $errorMessage = $invokeCimMethodResult.Error + } + + $hResult = $invokeCimMethodResult.ReturnValue + + if ($invokeCimMethodResult.HRESULT) + { + $hResult = $invokeCimMethodResult.HRESULT + } + + $errmsg = 'Method {0}() failed with an error. Error: {1} (HRESULT:{2})' -f @( + $MethodName + $errorMessage + $hResult + ) + 'ERROR: ' + $errmsg | Write-DscTrace + exit 1 + } + + return $invokeCimMethodResult +} + +class dscResourceObject +{ + [string] $name + [string] $type + [PSCustomObject] $properties + [Microsoft.Management.Infrastructure.CimInstance] $CimInstance +} \ No newline at end of file