diff --git a/build.ps1 b/build.ps1 index 8be253f31..8313b3351 100644 --- a/build.ps1 +++ b/build.ps1 @@ -119,6 +119,7 @@ $projects = @( "tools/test_group_resource", "y2j" "powershellgroup" + "wmigroup" "resources/brew" "tools/dsctest" ) diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index b99811095..f332d308b 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -334,7 +334,8 @@ impl Configurator { debug!("resource_type {}", &resource.resource_type); //TODO: remove this after schema validation for classic PS resources is implemented - if resource.resource_type == "DSC/PowerShellGroup" {continue;} + if (resource.resource_type == "DSC/PowerShellGroup") + || (resource.resource_type == "DSC/WMIGroup") {continue;} let input = serde_json::to_string(&resource.properties)?; let schema = match dsc_resource.schema() { diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index dfe50e31d..2c530dd1a 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -481,7 +481,8 @@ fn verify_json(resource: &ResourceManifest, cwd: &str, json: &str) -> Result<(), debug!("resource_type - {}", resource.resource_type); //TODO: remove this after schema validation for classic PS resources is implemented - if resource.resource_type == "DSC/PowerShellGroup" {return Ok(());} + if (resource.resource_type == "DSC/PowerShellGroup") + || (resource.resource_type == "DSC/WMIGroup") {return Ok(());} let schema = get_schema(resource, cwd)?; let schema: Value = serde_json::from_str(&schema)?; diff --git a/wmigroup/Tests/test_wmi_config.dsc.yaml b/wmigroup/Tests/test_wmi_config.dsc.yaml new file mode 100644 index 000000000..3bac984f5 --- /dev/null +++ b/wmigroup/Tests/test_wmi_config.dsc.yaml @@ -0,0 +1,13 @@ +# Example configuration for reading data from Windows WMI +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json +resources: +- name: Get info from WMI + type: DSC/WMIGroup + properties: + resources: + - name: Get OS Info + type: root.cimv2/Win32_OperatingSystem + - name: Get BIOS Info + type: root.cimv2/Win32_BIOS + - name: Get Processor Info + type: root.cimv2/Win32_Processor diff --git a/wmigroup/Tests/wmigroup.tests.ps1 b/wmigroup/Tests/wmigroup.tests.ps1 new file mode 100644 index 000000000..aa940deb2 --- /dev/null +++ b/wmigroup/Tests/wmigroup.tests.ps1 @@ -0,0 +1,54 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'PowerShellGroup resource tests' { + + BeforeAll { + if ($IsWindows) + { + $OldPSModulePath = $env:PSModulePath + $env:PSModulePath += ";" + $PSScriptRoot + + $configPath = Join-path $PSScriptRoot "test_wmi_config.dsc.yaml" + + $dscPath = (get-command dsc -CommandType Application).Path + $dscFolder = Split-Path -Path $dscPath + $wmiGroupOptoutFile = Join-Path $dscFolder "wmigroup.dsc.resource.json.optout" + $wmiGroupOptinFile = Join-Path $dscFolder "wmigroup.dsc.resource.json" + Rename-Item -Path $wmiGroupOptoutFile -NewName $wmiGroupOptinFile + } + } + AfterAll { + if ($IsWindows) + { + $env:PSModulePath = $OldPSModulePath + Rename-Item -Path $wmiGroupOptinFile -NewName $wmiGroupOptoutFile + } + } + + It 'List shows WMI resources' -Skip:(!$IsWindows){ + + $r = dsc resource list *OperatingSystem* + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.Count | Should -BeGreaterOrEqual 1 + } + + 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" + } + + 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[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 + } +} diff --git a/wmigroup/copy_files.txt b/wmigroup/copy_files.txt new file mode 100644 index 000000000..b00f55c8c --- /dev/null +++ b/wmigroup/copy_files.txt @@ -0,0 +1,2 @@ +wmigroup.resource.ps1 +wmigroup.dsc.resource.json.optout \ No newline at end of file diff --git a/wmigroup/wmigroup.dsc.resource.json.optout b/wmigroup/wmigroup.dsc.resource.json.optout new file mode 100644 index 000000000..ca37a7f32 --- /dev/null +++ b/wmigroup/wmigroup.dsc.resource.json.optout @@ -0,0 +1,37 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", + "type": "DSC/WMIGroup", + "version": "0.1.0", + "description": "Resource provider to WMI resources.", + "tags": [ + "PowerShell" + ], + "provider": { + "list": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "./wmigroup.resource.ps1 List" + ] + }, + "config": "full" + }, + "get": { + "executable": "powershell", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-Command", + "$Input | ./wmigroup.resource.ps1 Get" + ], + "input": "stdin" + }, + "exitCodes": { + "0": "Success", + "1": "Error" + } + } diff --git a/wmigroup/wmigroup.resource.ps1 b/wmigroup/wmigroup.resource.ps1 new file mode 100644 index 000000000..50ce84147 --- /dev/null +++ b/wmigroup/wmigroup.resource.ps1 @@ -0,0 +1,129 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[CmdletBinding()] +param( + [ValidateSet('List','Get','Set','Test')] + $Operation = 'List', + [Parameter(ValueFromPipeline)] + $stdinput +) + +$ProgressPreference = 'Ignore' +$WarningPreference = 'Ignore' +$VerbosePreference = 'Ignore' + +if ($Operation -eq 'List') +{ + $clases = Get-CimClass + + foreach ($r in $clases) + { + $version_string = ""; + $author_string = ""; + $moduleName = ""; + + $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 = "DSC/WMIGroup" + + $z = [pscustomobject]@{ + type = $fullResourceTypeName; + version = $version_string; + path = ""; + directory = ""; + implementedAs = ""; + author = $author_string; + properties = $propertyList; + requires = $requiresString + } + + $z | ConvertTo-Json -Compress + } +} +elseif ($Operation -eq 'Get') +{ + $inputobj_pscustomobj = $null + if ($stdinput) + { + $inputobj_pscustomobj = $stdinput | ConvertFrom-Json + } + + $result = @() + + if ($inputobj_pscustomobj.resources) # 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) + { + $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 + } + } + + $result += @($instance_result) + } + else + { + $errmsg = "Can not find type " + $r.type + "; please ensure that Get-CimInstance returns this resource type" + Write-Error $errmsg + exit 1 + } + } + } + else # we are processing an individual resource call + { + $type_fields = $inputobj_pscustomobj.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) + { + $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 + } + } + } + else + { + $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-CimInstance returns this resource type" + Write-Error $errmsg + exit 1 + } + } + + $result | ConvertTo-Json -Compress +} +else +{ + Write-Error "ERROR: Unsupported operation requested from wmigroup.resource.ps1" +} \ No newline at end of file