diff --git a/.vscode/settings.json b/.vscode/settings.json index fe5bf6730..a7ce35086 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,5 +26,6 @@ "vscode-nmake-tools.workspaceBuildDirectories": [ "." ], - "azure-pipelines.1ESPipelineTemplatesSchemaFile": true + "azure-pipelines.1ESPipelineTemplatesSchemaFile": true, + "powershell.codeFormatting.preset": "OTBS" } \ No newline at end of file diff --git a/wmi-adapter/copy_files.txt b/wmi-adapter/copy_files.txt index a6bfcb395..a96936ddd 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 edad7bd9a..ee80f7a59 100644 --- a/wmi-adapter/wmi.dsc.resource.json +++ b/wmi-adapter/wmi.dsc.resource.json @@ -1,7 +1,7 @@ { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "type": "Microsoft.Windows/WMI", - "version": "0.1.0", + "version": "1.0.0", "kind": "adapter", "description": "Resource adapter to WMI resources.", "tags": [ diff --git a/wmi-adapter/wmi.resource.ps1 b/wmi-adapter/wmi.resource.ps1 index 125d41190..726b5d556 100644 --- a/wmi-adapter/wmi.resource.ps1 +++ b/wmi-adapter/wmi.resource.ps1 @@ -1,170 +1,110 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - [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, Validate.')] + [ValidateSet('List', 'Get', 'Set', 'Test', 'Validate')] + [string]$Operation, + [Parameter(Mandatory = $false, Position = 1, ValueFromPipeline = $true, HelpMessage = 'Configuration or resource input in JSON format.')] + [string]$jsonInput = '@{}' ) -# catch any un-caught exception and write it to the error stream -trap { - Write-Trace -Level Error -message $_.Exception.Message - exit 1 -} - -$ProgressPreference = 'Ignore' -$WarningPreference = 'Ignore' -$VerbosePreference = 'Ignore' - -function Write-Trace { - param( - [string]$message, - [string]$level = 'Error' - ) - - $trace = [pscustomobject]@{ - $level.ToLower() = $message - } | ConvertTo-Json -Compress - - $host.ui.WriteErrorLine($trace) -} - -if ($Operation -eq 'List') -{ - $clases = Get-CimClass - - foreach ($r in $clases) - { - $version_string = ""; - $author_string = ""; - - $propertyList = @() - foreach ($p in $r.CimClassProperties) - { - if ($p.Name) - { - $propertyList += $p.Name - } - } - - $namespace = $r.CimSystemProperties.Namespace.ToLower().Replace('/','.') - $classname = $r.CimSystemProperties.ClassName - $fullResourceTypeName = "$namespace/$classname" - $requiresString = "Microsoft.Windows/WMI" +# Import private functions +$wmiAdapter = Import-Module "$PSScriptRoot/wmiAdapter.psm1" -Force -PassThru - $z = [pscustomobject]@{ - type = $fullResourceTypeName; - kind = 'resource'; - version = $version_string; - capabilities = @('get'); - path = ""; - directory = ""; - implementedAs = ""; - author = $author_string; - properties = $propertyList; - requireAdapter = $requiresString - } +if ('Validate' -ne $Operation) { + # initialize OUTPUT as array + $result = [System.Collections.Generic.List[Object]]::new() - $z | ConvertTo-Json -Compress - } + Write-DscTrace -Operation Debug -Message "jsonInput=$jsonInput" } -elseif ($Operation -eq 'Get') -{ - $inputobj_pscustomobj = $null - if ($stdinput) - { - $inputobj_pscustomobj = $stdinput | ConvertFrom-Json - } - $result = @() +# Adding some debug info to STDERR +'PSVersion=' + $PSVersionTable.PSVersion.ToString() | Write-DscTrace +'PSPath=' + $PSHome | Write-DscTrace +'PSModulePath=' + $env:PSModulePath | Write-DscTrace - foreach ($r in $inputobj_pscustomobj.resources) - { - $type_fields = $r.type -split "/" - $wmi_namespace = $type_fields[0].Replace('.','\') - $wmi_classname = $type_fields[1] +switch ($Operation) { + 'List' { + $clases = Get-CimClass - # 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 " - } + foreach ($r in $clases) { + $version_string = "" + $author_string = "" + $description = "" - if ($property.TypeNameOfValue -eq "System.String") - { - $where += "$($property.Name) = '$($property.Value)'" - } - else - { - $where += "$($property.Name) = $($property.Value)" - } + $propertyList = @() + foreach ($p in $r.CimClassProperties) { + if ($p.Name) { + $propertyList += $p.Name } } - if ($useWhere) - { - $query += $where - } - Write-Trace -Level Trace -message "Query: $query" - $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -Query $query -ErrorAction Stop + + $namespace = $r.CimSystemProperties.Namespace.ToLower().Replace('/', '.') + $classname = $r.CimSystemProperties.ClassName + $fullResourceTypeName = "$namespace/$classname" + $requiresString = "Microsoft.Windows/WMI" + + # OUTPUT dsc is expecting the following properties + [resourceOutput]@{ + type = $fullResourceTypeName + kind = 'resource' + version = $version_string + capabilities = @('get', 'set', 'test') + path = "" + directory = "" + implementedAs = "" + author = $author_string + properties = $propertyList + requireAdapter = $requiresString + description = $description + } | ConvertTo-Json -Compress } - else - { - $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname -ErrorAction Stop + } + { @('Get', 'Set', 'Test') -contains $_ } { + $desiredState = $wmiAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) + if ($null -eq $desiredState) { + "Failed to create configuration object from provided input JSON." | Write-DscTrace -Operation Error + exit 1 } - if ($wmi_instances) - { - $instance_result = [ordered]@{} - # TODO: for a `Get`, they key property must be provided so a specific instance is returned rather than just the first - $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"))) - { - if ($r.properties) - { - if ($r.properties.psobject.properties.name -contains $_.Name) - { - $instance_result[$_.Name] = $_.Value - } - } - else - { - $instance_result[$_.Name] = $_.Value - } - } + foreach ($ds in $desiredState) { + # 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) { + "Incomplete GET for resource $($ds.Type)" | Write-DscTrace -Operation Error + exit 1 } - $result += [pscustomobject]@{ name = $r.name; type = $r.type; properties = $instance_result } + $result += $actualstate } - } - @{result = $result } | ConvertTo-Json -Depth 10 -Compress -} -elseif ($Operation -eq 'Validate') -{ - # TODO: this is placeholder - @{ valid = $true } | ConvertTo-Json + # OUTPUT json to stderr for debug, and to stdout + "jsonOutput=$($result | ConvertTo-Json -Depth 10 -Compress)" | Write-DscTrace -Operation Debug + return (@{ result = $result } | ConvertTo-Json -Depth 10 -Compress) + } + 'Validate' { + # TODO: VALIDATE not implemented + + # OUTPUT + @{ valid = $true } | ConvertTo-Json + } + Default { + Write-DscTrace -Operation Error -Message 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' + } } -else -{ - Write-Trace "ERROR: Unsupported operation requested from wmigroup.resource.ps1" + +# output format for resource list +class resourceOutput { + [string] $type + [string] $kind + [string] $version + [string[]] $capabilities + [string] $path + [string] $directory + [string] $implementedAs + [string] $author + [string[]] $properties + [string] $requireAdapter + [string] $description } diff --git a/wmi-adapter/wmiAdapter.psd1 b/wmi-adapter/wmiAdapter.psd1 new file mode 100644 index 000000000..d0f275693 --- /dev/null +++ b/wmi-adapter/wmiAdapter.psd1 @@ -0,0 +1,52 @@ +# 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 = '1.0.0' + + # 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 = @{ + DscCapabilities = @( + 'get' + 'test' + 'set' + ) + + 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 000000000..3b4919974 --- /dev/null +++ b/wmi-adapter/wmiAdapter.psm1 @@ -0,0 +1,168 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +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.ToLower() = $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 + $desiredState = [System.Collections.Generic.List[Object]]::new() + + $inputObj.resources | ForEach-Object -Process { + $desiredState += [dscResourceObject]@{ + name = $_.name + type = $_.type + properties = $_.properties + } + } + + return $desiredState +} + +function GetCimSpace { + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateSet('Get', 'Set', 'Test')] + [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) } + } + + $result = @() + + 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 + } + + if ($wmi_instances) { + $instance_result = [ordered]@{} + # TODO: for a `Get`, they key property must be provided so a specific instance is returned rather than just the first + $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"))) { + if ($r.properties) { + if ($r.properties.psobject.properties.name -contains $_.Name) { + $instance_result[$_.Name] = $_.Value + } + } else { + $instance_result[$_.Name] = $_.Value + } + } + } + + $addToActualState.properties = $instance_result + + $result += $addToActualState + } + + } + 'Set' { + # TODO: implement set + + } + 'Test' { + # TODO: implement test + } + } + } + + return $result +} + + +function Invoke-DscWmi { + [CmdletBinding()] + param + ( + [Parameter(Mandatory)] + [ValidateSet('Get', 'Set', 'Test', 'Export')] + [System.String] + $Operation, + + [Parameter(Mandatory, ValueFromPipeline = $true)] + [dscResourceObject] + $DesiredState + ) + + switch ($Operation) { + 'Get' { + $addToActualState = GetCimSpace -Operation $Operation -DesiredState $DesiredState + } + 'Set' { + # TODO: Implement Set operation + } + 'Test' { + # TODO: Implement Test operation + } + } + + return $addToActualState +} + +class dscResourceObject { + [string] $name + [string] $type + [PSCustomObject] $properties +} \ No newline at end of file