Skip to content

Add WMI set capability #946

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions wmi-adapter/Tests/wmi.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,40 @@ Describe 'WMI adapter resource tests' {
$res.results[1].result.actualState.result[1].properties.BuildNumber | Should -BeNullOrEmpty
$res.results[1].result.actualState.result[4].properties.AdapterType | Should -BeLike "Ethernet*"
}

It 'Throws error when methodName is missing on set' -Skip:(!$IsWindows) {
'{"Name":"wuauserv"}' | dsc -l trace resource set -r 'root.cimv2/Win32_Service' -f - 2> $TestDrive\tracing.txt
$LASTEXITCODE | Should -Be 2
"$TestDrive/tracing.txt" | Should -FileContentMatch "'methodName' property is required for invoking a WMI/CIM method."
}

It 'Throws error when methodName is set but parameters are missing on set' -Skip:(!$IsWindows) {
'{"Name":"wuauserv", "methodName":"StartService"}' | dsc -l trace resource set -r 'root.cimv2/Win32_Service' -f - 2> $TestDrive\tracing.txt
$LASTEXITCODE | Should -Be 2
"$TestDrive/tracing.txt" | Should -FileContentMatch "'parameters' property is required for invoking a WMI/CIM method."
}

It 'Set works on a WMI resource with methodName and parameters' -Skip:(!$IsWindows) {
BeforeAll {
$script:service = Get-Service -Name wuauserv -ErrorAction Ignore

if ($service -and $service.Status -eq 'Running') {
$service.Stop()
}
}


$r = '{"Name":"wuauserv", "methodName":"StartService", "parameters":{}}' | dsc resource set -r 'root.cimv2/Win32_Service' -f -
$LASTEXITCODE | Should -Be 0

$res = '{"Name":"wuauserv", "State": null}' | dsc resource get -r 'root.cimv2/Win32_Service' -f - | ConvertFrom-Json
$res.actualState.Name | Should -Be 'wuauserv'
$res.actualState.State | Should -Be 'Running'

AfterAll {
if ($service -and $service.Status -eq 'Running') {
$service.Stop()
}
}
}
}
13 changes: 13 additions & 0 deletions wmi-adapter/wmi.dsc.resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@
],
"input": "stdin"
},
"set": {
"executable": "powershell",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"$Input | ./wmi.resource.ps1 Set"
],
"input": "stdin"
},
"validate": {
"executable": "powershell",
"args": [
Expand Down
253 changes: 207 additions & 46 deletions wmi-adapter/wmiAdapter.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,53 @@ function Get-DscResourceObject {
return $desiredState
}

function GetWmiInstance {
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[psobject]$DesiredState
)

$type_fields = $DesiredState.type -split "/"
$wmi_namespace = $type_fields[0].Replace('.', '\')
$wmi_classname = $type_fields[1]

if ($DesiredState.properties) {
$props = $DesiredState.properties.psobject.Properties | Where-Object { $_.Name -notin @('methodName', 'parameters') }
$query = "SELECT $($props.Name -join ',') FROM $wmi_classname"
$where = " WHERE "
$useWhere = $false
$first = $true
foreach ($property in $props) {
# TODO: validate property against the CIM class to give better error message
if ($null -ne $property.value) {
$useWhere = $true
if ($first) {
$first = $false
} else {
$where += " AND "
}

if ($property.TypeNameOfValue -eq "System.String") {
$where += "$($property.Name) = '$($property.Value)'"
} else {
$where += "$($property.Name) = $($property.Value)"
}
}
}
if ($useWhere) {
$query += $where
}
"Query: $query" | Write-DscTrace -Operation Debug
$wmi_instances = Get-CimInstance -Namespace $wmi_namespace -Query $query -ErrorAction Stop
} else {
$wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname -ErrorAction Stop
}

return $wmi_instances
}

function GetCimSpace {
[CmdletBinding()]
param
Expand All @@ -58,43 +105,10 @@ function GetCimSpace {

foreach ($r in $DesiredState) {

$type_fields = $r.type -split "/"
$wmi_namespace = $type_fields[0].Replace('.', '\')
$wmi_classname = $type_fields[1]

switch ($Operation) {
'Get' {
# TODO: identify key properties and add WHERE clause to the query
if ($r.properties) {
$query = "SELECT $($r.properties.psobject.properties.name -join ',') FROM $wmi_classname"
$where = " WHERE "
$useWhere = $false
$first = $true
foreach ($property in $r.properties.psobject.properties) {
# TODO: validate property against the CIM class to give better error message
if ($null -ne $property.value) {
$useWhere = $true
if ($first) {
$first = $false
} else {
$where += " AND "
}

if ($property.TypeNameOfValue -eq "System.String") {
$where += "$($property.Name) = '$($property.Value)'"
} else {
$where += "$($property.Name) = $($property.Value)"
}
}
}
if ($useWhere) {
$query += $where
}
"Query: $query" | Write-DscTrace -Operation Debug
$wmi_instances = Get-CimInstance -Namespace $wmi_namespace -Query $query -ErrorAction Stop
} else {
$wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname -ErrorAction Stop
}
$wmi_instances = GetWmiInstance -DesiredState $DesiredState

if ($wmi_instances) {
$instance_result = [ordered]@{}
Expand All @@ -119,18 +133,175 @@ function GetCimSpace {

}
'Set' {
# TODO: implement set
$wmi_instance = ValidateCimMethodAndArguments -DesiredState $r
InvokeCimMethod @wmi_instance

$addToActualState = [dscResourceObject]@{
name = $r.name
type = $r.type
properties = $null
}

$result += $addToActualState
}
'Test' {
# TODO: implement test
"Test operation is not implemented for WMI/CIM methods." | Write-DscTrace -Operation Error
exit 1
}
}
}

return $result
}

function ValidateCimMethodAndArguments {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[dscResourceObject]$DesiredState
)

$methodName = $DesiredState.properties.psobject.properties | Where-Object -Property Name -EQ 'methodName' | Select-Object -ExpandProperty Value
if (-not $methodName) {
"'methodName' property is required for invoking a WMI/CIM method." | Write-DscTrace -Operation Error
exit 1
}

# This is required for invoking a WMI/CIM method with parameters even if it is empty
if (-not ($DesiredState.properties.psobject.properties | Where-Object -Property Name -EQ 'parameters')) {
"'parameters' property is required for invoking a WMI/CIM method." | Write-DscTrace -Operation Error
exit 1
}

$className = $DesiredState.type.Split("/")[-1]
$namespace = $DesiredState.type.Split("/")[0].Replace(".", "/")

$cimClass = Get-CimClass -Namespace $namespace -ClassName $className -MethodName $methodName

$arguments = @{}
if ($cimClass) {
$parameters = ($DesiredState.properties.psobject.properties | Where-Object -Property Name -EQ 'parameters').Value
$cimClassParameters = $cimClass.CimClassMethods | Where-Object -Property Name -EQ $methodName | Select-Object -ExpandProperty Parameters

foreach ($param in $parameters.psobject.Properties.name) {
if ($cimClassParameters.Name -notcontains $param) {
# Only warn about invalid parameters, do not exit as this allows to action to continue when calling InvokeCimMethod
"'$param' is not a valid parameter for method '$methodName' in class '$className'." | Write-DscTrace -Operation Warn
} else {
$arguments += @{
$param = $parameters.$param
}
}
}

$cimInstance = GetWmiInstance -DesiredState $DesiredState

return @{
CimInstance = $cimInstance
Arguments = $arguments
MethodName = $methodName
}
} else {
"'$className' class not found in namespace '$namespace'." | Write-DscTrace -Operation Error
exit 1
}
}

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') -and $null -ne [string]::IsNullOrEmpty($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
{
"Could not execute 'Invoke-CimMethod' with error message: " + $_.Exception.Message | Write-DscTrace -Operation Error
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
)
$errMsg | Write-DscTrace -Operation Error
exit 1
}
}


function Invoke-DscWmi {
[CmdletBinding()]
Expand All @@ -146,17 +317,7 @@ function Invoke-DscWmi {
$DesiredState
)

switch ($Operation) {
'Get' {
$addToActualState = GetCimSpace -Operation $Operation -DesiredState $DesiredState
}
'Set' {
# TODO: Implement Set operation
}
'Test' {
# TODO: Implement Test operation
}
}
$addToActualState = GetCimSpace -Operation $Operation -DesiredState $DesiredState

return $addToActualState
}
Expand Down
Loading