From 3d98c62a242c445980b8e095904c7997c7006385 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Wed, 24 Jul 2024 13:22:19 +0200 Subject: [PATCH 1/7] DSC Configuration Migration Tool module --- powershell-helpers/README.md | 13 + powershell-helpers/dscCfgMigMod.psd1 | 47 +++ powershell-helpers/dscCfgMigMod.psm1 | 384 ++++++++++++++++++ .../tests/dscCfgMigMod.tests.ps1 | 24 ++ 4 files changed, 468 insertions(+) create mode 100644 powershell-helpers/README.md create mode 100644 powershell-helpers/dscCfgMigMod.psd1 create mode 100644 powershell-helpers/dscCfgMigMod.psm1 create mode 100644 powershell-helpers/tests/dscCfgMigMod.tests.ps1 diff --git a/powershell-helpers/README.md b/powershell-helpers/README.md new file mode 100644 index 000000000..d48f4e871 --- /dev/null +++ b/powershell-helpers/README.md @@ -0,0 +1,13 @@ +# Introduction + +The `powershell-adapters` folder contains helper modules that can be loaded into your PowerShell session to assist you in familiarizing yourself with new DSC concepts. To see the availability of helper modules, see the following list: + +- **DSC Configuration Migration Module**: - Aids in the assistance of grabbing configuration documents written in PowerShell code and transform them to valid configuration documents for the DSC version 3 core engine (e.g. YAML or JSON). + +## Getting started + +To get started using the helper modules, you can follow the below steps. This example uses the _DSC Configuration Migration Tool_ to be loaded into the session: + +1. Open a PowerShell terminal session +2. Execute the following command: `Import-Module "powershell-helpers\dscConfigurationMigrationTool.psm1"` +3. Discover examples using: `Get-Help ConvertTo-DscYaml` diff --git a/powershell-helpers/dscCfgMigMod.psd1 b/powershell-helpers/dscCfgMigMod.psd1 new file mode 100644 index 000000000..83d1ac091 --- /dev/null +++ b/powershell-helpers/dscCfgMigMod.psd1 @@ -0,0 +1,47 @@ +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'dscCfgMigMod.psm1' + + # Version number of this module. + moduleVersion = '0.0.1' + + # ID used to uniquely identify this module + GUID = '42bf8cb0-210c-4dac-8614-319d9287c6dc' + + # 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 Migration Module helper' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('powershell-yaml') + + # 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 = @( + 'ConvertTo-DscJson' + 'ConvertTo-DscYaml' + ) + + # 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' + } + } +} diff --git a/powershell-helpers/dscCfgMigMod.psm1 b/powershell-helpers/dscCfgMigMod.psm1 new file mode 100644 index 000000000..c61ff4bff --- /dev/null +++ b/powershell-helpers/dscCfgMigMod.psm1 @@ -0,0 +1,384 @@ +#region Main functions +function ConvertTo-DscJson +{ + <# + .SYNOPSIS + Convert a PowerShell DSC configuration document to DSC version 3 JSON format. + + .DESCRIPTION + The function ConvertTo-DscJson converts a PowerShell DSC configuration document to DSC version 3 JSON format from a path. + + .PARAMETER Path + The path to valid PowerShell DSC configuration document + + .EXAMPLE + PS C:\> $configuration = @' + Configuration TestResource { + Import-DscResource -ModuleName TestResource + Node localhost { + TestResource 'Configure test resource' { + Ensure = 'Absent' + Name = 'MyTestResource' + } + } + } + '@ + PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' + PS C:\> $configuration | Out-File -FilePath $Path + PS C:\> ConvertTo-DscJson -Path $Path + + Returns: + { + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", + "resources": { + "name": "TestResource", + "type": "Microsoft.DSC/PowerShell", + "properties": { + "resources": [ + { + "name": "Configure test resource", + "type": "TestResource/TestResource", + "properties": { + "Name": "MyTestResource", + "Ensure": "Absent" + } + } + ] + } + } + } + + .NOTES + Tags: DSC, Migration, JSON + #> + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + begin + { + Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) + } + + process + { + $inputObject = BuildConfigurationDocument -Path $Path + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} + +function ConvertTo-DscYaml +{ + <# + .SYNOPSIS + Convert a PowerShell DSC configuration document to DSC version 3 YAML format. + + .DESCRIPTION + The function ConvertTo-DscYaml converts a PowerShell DSC configuration document to DSC version 3 YAML format from a path. + + .PARAMETER Path + The path to valid PowerShell DSC configuration document + + .EXAMPLE + PS C:\> $configuration = @' + Configuration TestResource { + Import-DscResource -ModuleName TestResource + Node localhost { + TestResource 'Configure test resource' { + Ensure = 'Absent' + Name = 'MyTestResource' + } + } + } + '@ + PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' + PS C:\> $configuration | Out-File -FilePath $Path + PS C:\> ConvertTo-DscYaml -Path $Path + + Returns: + $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json + resources: + name: TestResource + type: Microsoft.DSC/PowerShell + properties: + resources: + - name: Configure test resource + type: TestResource/TestResource + properties: + Name: MyTestResource + Ensure: Absent + + .NOTES + Tags: DSC, Migration, YAML + #> + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + begin + { + Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) + } + + process + { + $inputObject = BuildConfigurationDocument -Path $Path -Format YAML + } + end + { + Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) + return $inputObject + } +} +#endRegion Main functions + +#region Helper functions +function FindAndExtractConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + if (-not (TestPathExtension $Path)) + { + return @{} + } + + # Parse the abstract syntax tree to get all hash table values representing the configuration resources + [System.Management.Automation.Language.Token[]] $tokens = $null + [System.Management.Automation.Language.ParseError[]] $errors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) + $configurations = $ast.FindAll({$args[0].GetType().Name -like 'HashtableAst'}, $true) + + # Create configuration document resource class (can be re-used) + $configurationDocument = [DscConfigurationResource]::new() + + # Build simple regex + $regex = [regex]::new('Configuration\s+(\w+)') + $configValue = $regex.Matches($ast.Extent.Text).Value + + if (-not $configValue) + { + return + } + + $documentConfigurationName = $configValue.TrimStart('Configuration').Trim(" ") + + # Start to build the outer basic format + $configurationDocument.name = $documentConfigurationName + $configurationDocument.type = 'Microsoft.DSC/PowerShell' # TODO: Add functions later to valid the adapter type + + # Bag to hold resources + $resourceProps = [System.Collections.Generic.List[object]]::new() + + foreach ($configuration in $configurations) + { + # Get parent configuration details + $resourceName = ($configuration.Parent.CommandElements.Value | Select-Object -Last 1 ) + $resourceConfigurationName = ($configuration.Parent.CommandElements.Value | Select-Object -First 1) + + # Get module details + $module = Get-DscResource -Name $resourceConfigurationName -ErrorAction SilentlyContinue + + # Build the module + $resource = [DscConfigurationResource]::new() + $resource.properties = $configuration.SafeGetValue() + $resource.name = $resourceName + $resource.type = ("{0}/{1}" -f $module.ModuleName, $resourceConfigurationName) + # TODO: Might have to change because it takes time. If there is only one Import-DscResource statement, we can simply RegEx it out, else use Get-DscResource + # $document.ModuleName = $module.ModuleName + + Write-Verbose ("Adding document with data") + Write-Verbose ($resource | ConvertTo-Json | Out-String) + $resourceProps.Add($resource) + } + + # Add all the resources + $configurationDocument.properties = @{ + resources = $resourceProps + } + + return $configurationDocument +} + +function BuildConfigurationDocument +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [ValidateSet('JSON', 'YAML')] + [System.String] + $Format = 'JSON' + ) + + $configurationDocument = [ordered]@{ + "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" # TODO: Figure out how to extract latest document.json from schemas folder + resources = FindAndExtractConfigurationDocument -Path $Path + } + + switch ($Format) + { + "JSON" { + $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) + } + "YAML" { + if (TestYamlModule) + { + $inputObject = ($configurationDocument | ConvertTo-Yaml) + } + else + { + $inputObject = @{} + } + } + default { + $inputObject = $configurationDocument + } + } + + return $inputObject +} + +function TestPathExtension +{ + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path + ) + + $res = $true + + if (-not (Test-Path $Path)) + { + $res = $false + } + + if (([System.IO.Path]::GetExtension($Path) -ne ".ps1")) + { + $res = $false + } + + return $res +} + +function TestYamlModule +{ + if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) + { + return $false + } + + return $true +} + +function GetPowerShellPath +{ + param + ( + $Path + ) + + $knownPath = @( + "$env:USERPROFILE\Documents\PowerShell\Modules", + "$env:ProgramFiles\PowerShell\Modules", + "$env:ProgramFiles\PowerShell\7\Modules" + ) + + foreach ($known in $knownPath) + { + if ($Path.StartsWith($known)) + { + return $true + } + } + + return $false +} + +function GetWindowsPowerShellPath +{ + param + ( + $Path + ) + + $knownPath = @( + "$env:USERPROFILE\Documents\WindowsPowerShell\Modules", + "$env:ProgramFiles\WindowsPowerShell\Modules", + "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules" + ) + + foreach ($known in $knownPath) + { + if ($Path.StartsWith($known)) + { + return $true + } + } + + return $false +} + +function ResolvePowerShellPath +{ + [CmdletBinding()] + Param + ( + [System.String] + $Path + ) + + if (-not (Test-Path $Path)) + { + return + } + + if (([System.IO.Path]::GetExtension($Path) -ne ".psm1")) + { + return + } + + if (GetPowerShellPath -Path $Path) + { + return "Microsoft.DSC/PowerShell" + } + + if (GetWindowsPowerShellPath -Path $Path) + { + return "Microsoft.Windows/WindowsPowerShell" + } + + return $null # TODO: Or default Microsoft.DSC/PowerShell +} + +#endRegion Helper functions + +#region Classes +class DscConfigurationResource +{ + [string] $name + [string] $type + [hashtable] $properties +} +#endRegion classes \ No newline at end of file diff --git a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 new file mode 100644 index 000000000..b966993ad --- /dev/null +++ b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 @@ -0,0 +1,24 @@ +Describe "DSC Configuration Migration Module tests" { + BeforeAll { + $modPath = (Resolve-Path -Path "$PSScriptRoot\..\dscCfgMigMod.psd1").Path + $modLoad = Import-Module $modPath -Force -PassThru + } + + Context "ConvertTo-DscYaml" { + It "Should create an empty resource block" { + $res = (ConvertTo-DscYaml -Path 'idonotexist' | ConvertFrom-Yaml) + $res.resources | Should -BeNullOrEmpty + } + } + + Context "ConvertTo-DscJson" { + It "Should create an empty resource block" { + $res = (ConvertTo-DscJson -Path 'idonotexist' | ConvertFrom-Json) + $res.resources | Should -BeNullOrEmpty + } + } + + AfterAll { + Remove-Module -Name $modLoad.Name -Force + } +} From 9443e1cdd619f85cdb29606093b449e65cd67c6e Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Thu, 25 Jul 2024 17:11:07 +0200 Subject: [PATCH 2/7] Add class-based operation methods in DSCResourceInfo --- .../psDscAdapter/psDscAdapter.psm1 | 270 +++++++++++++----- 1 file changed, 196 insertions(+), 74 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index 07b3763e8..f31f672d9 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -22,25 +22,19 @@ function Import-PSDSCModule { $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } -function Get-DSCResourceModules -{ +function Get-DSCResourceModules { $listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator) $dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new() - foreach ($folder in $listPSModuleFolders) - { - if (!(Test-Path $folder)) - { + foreach ($folder in $listPSModuleFolders) { + if (!(Test-Path $folder)) { continue } - foreach($moduleFolder in Get-ChildItem $folder -Directory) - { + foreach ($moduleFolder in Get-ChildItem $folder -Directory) { $addModule = $false - foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) - { + foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) { $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' - if($null -ne $containsDSCResource) - { + if ($null -ne $containsDSCResource) { $dscModulePsd1List.Add($psd1) | Out-Null } } @@ -57,39 +51,32 @@ function Add-AstMembers { $Properties ) - foreach($TypeConstraint in $TypeAst.BaseTypes) { - $t = $AllTypeDefinitions | Where-Object {$_.Name -eq $TypeConstraint.TypeName.Name} + foreach ($TypeConstraint in $TypeAst.BaseTypes) { + $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } if ($t) { Add-AstMembers $AllTypeDefinitions $t $Properties } } - foreach ($member in $TypeAst.Members) - { + foreach ($member in $TypeAst.Members) { $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] - if (($property -eq $null) -or ($property.IsStatic)) - { + if (($property -eq $null) -or ($property.IsStatic)) { continue; } $skipProperty = $true $isKeyProperty = $false - foreach($attr in $property.Attributes) - { - if ($attr.TypeName.Name -eq 'DscProperty') - { + foreach ($attr in $property.Attributes) { + if ($attr.TypeName.Name -eq 'DscProperty') { $skipProperty = $false - foreach($attrArg in $attr.NamedArguments) - { - if ($attrArg.ArgumentName -eq 'Key') - { + foreach ($attrArg in $attr.NamedArguments) { + if ($attrArg.ArgumentName -eq 'Key') { $isKeyProperty = $true break } } } } - if ($skipProperty) - { + if ($skipProperty) { continue; } @@ -101,8 +88,7 @@ function Add-AstMembers { } } -function FindAndParseResourceDefinitions -{ +function FindAndParseResourceDefinitions { [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -111,13 +97,11 @@ function FindAndParseResourceDefinitions [string]$moduleVersion ) - if (-not (Test-Path $filePath)) - { + if (-not (Test-Path $filePath)) { return } - if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) - { + if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) { return } @@ -126,8 +110,7 @@ function FindAndParseResourceDefinitions [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors) - foreach($e in $errors) - { + foreach ($e in $errors) { $e | Out-String | Write-DscTrace -Operation Error } @@ -140,12 +123,9 @@ function FindAndParseResourceDefinitions $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() - foreach($typeDefinitionAst in $typeDefinitions) - { - foreach($a in $typeDefinitionAst.Attributes) - { - if ($a.TypeName.Name -eq 'DscResource') - { + foreach ($typeDefinitionAst in $typeDefinitions) { + foreach ($a in $typeDefinitionAst.Attributes) { + if ($a.TypeName.Name -eq 'DscResource') { $DscResourceInfo = [DscResourceInfo]::new() $DscResourceInfo.Name = $typeDefinitionAst.Name $DscResourceInfo.ResourceType = $typeDefinitionAst.Name @@ -157,8 +137,10 @@ function FindAndParseResourceDefinitions $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) $DscResourceInfo.Version = $moduleVersion + $DscResourceInfo.Operations = GetResourceOperationMethods -resourceName $typeDefinitionAst.Name -filePath $filePath $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() + Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties $resourceList.Add($DscResourceInfo) @@ -169,8 +151,7 @@ function FindAndParseResourceDefinitions return $resourceList } -function LoadPowerShellClassResourcesFromModule -{ +function LoadPowerShellClassResourcesFromModule { [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -179,29 +160,24 @@ function LoadPowerShellClassResourcesFromModule "Loading resources from module '$($moduleInfo.Path)'" | Write-DscTrace -Operation Trace - if ($moduleInfo.RootModule) - { - if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and + if ($moduleInfo.RootModule) { + if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and ([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".ps1") -and - (-not $z.NestedModules)) - { + (-not $z.NestedModules)) { "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" | Write-DscTrace -Operation Trace return [System.Collections.Generic.List[DscResourceInfo]]::new() } $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule } - else - { + else { $scriptPath = $moduleInfo.Path; } $Resources = FindAndParseResourceDefinitions $scriptPath $moduleInfo.Version - if ($moduleInfo.NestedModules) - { - foreach ($nestedModule in $moduleInfo.NestedModules) - { + if ($moduleInfo.NestedModules) { + foreach ($nestedModule in $moduleInfo.NestedModules) { $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule if ($resourcesOfNestedModules) { $Resources.AddRange($resourcesOfNestedModules) @@ -212,6 +188,153 @@ function LoadPowerShellClassResourcesFromModule return $Resources } +function GetResourceOperationMethods { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $resourceName, + + [Parameter(Mandatory = $true)] + [string] $filePath + ) + + # dot source scope + try { + . (LoadClassAndEnumsFromModuleFile -filePath $filePath) + } catch { + ("Module: '{0}' not loaded for resource operation discovery."-f $filePath) | Write-DscTrace + } + + $inputObject = ReturnTypeNameObject -TypeName $resourceName + + if (-not $inputObject) { + return @( + 'Get', + 'Test', + 'Set' + ) + } + + # TODO: There might be more properties available + $knownMemberTypes = @('Equals', 'GetHashCode', 'GetType', 'ToString') + return ($inputObject | Get-Member | Where-Object { $_.MemberType -eq 'Method' -and $_.Name -notin $knownMemberTypes }).Name +} + +function ReturnTypeNameObject { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $TypeName + ) + + try { + $inputObject = New-Object -TypeName $TypeName -ErrorAction Stop + } + catch { + "Could not create: $TypeName" | Write-DscTrace + } + + return $inputObject +} + +function LoadClassAndEnumsFromModuleFile { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $filePath + ) + + if (-not (Test-Path $filePath -ErrorAction SilentlyContinue)) { + return + } + + $ctx = Get-Content $filePath + + $string = @( + 'using namespace System.Collections.Generic', # TODO: Figure away out to get using statements included + (GetEnumCodeBlock -Content $ctx), + (GetClassCodeBlock -Content $ctx) + ) + + # TODO: Might have to do something with the path + $outPath = Join-Path -Path $env:TEMP -ChildPath ("{0}.ps1" -f [System.Guid]::NewGuid().Guid) + $string | Out-File -FilePath $outPath + + return $outPath +} + +function GetClassCodeBlock { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content + ) + + $ctx = $Content + + $lines = ($ctx | Select-String -Pattern '\[DSCResource\(\)]').LineNumber + if ($lines.Count -eq 0 ) { + return + } + + $lastLineNumber = $lines[-1] + $index = 1 + # Bring all class strings together after the last one + $classStrings = foreach ($line in $lines) { + if ($line -eq $lastLineNumber) { + $lastModuleLine = $ctx.Length + + $line = $line - 1 + $block = $ctx[$line..$lastModuleLine] + $block + break + } + + $line = $line - 1 + $curlyBracketLine = FindCurlyBracket -Content $ctx -LineNumber $lines[$index] + $block = $ctx[$line..$curlyBracketLine] + + $index++ + $block + } + + return $classStrings +} + +function GetEnumCodeBlock { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content + ) + + # Build regex to catch enum blocks + $regex = [regex]::new('enum\s+(\w+)\s*\{([^}]+)\}') + + $hits = $regex.Matches($Content) + + # return as single lines + return ($hits.Value -Split " ") +} + +function FindCurlyBracket { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $Content, + + [Parameter(Mandatory = $true)] + [int] $LineNumber + ) + do { + if ($Content[$LineNumber] -eq "}") { + return $LineNumber + } + + $LineNumber-- + } while ($LineNumber -ne 0) +} + <# public function Invoke-DscCacheRefresh .SYNOPSIS This function caches the results of the Get-DscResource call to optimize performance. @@ -237,7 +360,8 @@ function Invoke-DscCacheRefresh { $cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } else { + } + else { # PS 6+ on Linux/Mac Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } @@ -249,8 +373,9 @@ function Invoke-DscCacheRefresh { if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '"+$cache.CacheSchemaVersion+"' (expected '"+$script:CurrentCacheSchemaVersion+"')" | Write-DscTrace - } else { + "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace + } + else { $dscResourceCacheEntries = $cache.ResourceCache if ($dscResourceCacheEntries.Count -eq 0) { @@ -259,8 +384,7 @@ function Invoke-DscCacheRefresh { "Filtered DscResourceCache cache is empty" | Write-DscTrace } - else - { + else { "Checking cache for stale entries" | Write-DscTrace foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -268,20 +392,19 @@ function Invoke-DscCacheRefresh { $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { - if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) - { + if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) { "Detected stale cache entry '$($_.Name)'" | Write-DscTrace $refreshCache = $true break } } - if ($refreshCache) {break} + if ($refreshCache) { break } } "Checking cache for stale PSModulePath" | Write-DscTrace - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) @@ -309,11 +432,10 @@ function Invoke-DscCacheRefresh { $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() $dscResourceModulePsd1s = Get-DSCResourceModules - if($null -ne $dscResourceModulePsd1s) { + if ($null -ne $dscResourceModulePsd1s) { $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) $processedModuleNames = @{} - foreach ($mod in $modules) - { + foreach ($mod in $modules) { if (-not ($processedModuleNames.ContainsKey($mod.Name))) { $processedModuleNames.Add($mod.Name, $true) @@ -337,20 +459,20 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1","*.psd1","*psm1","*.mof" -ea Ignore | % { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*psm1", "*.mof" -ea Ignore | % { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } $dscResourceCacheEntries += [dscResourceCacheEntry]@{ Type = "$moduleName/$($dscResource.Name)" DscResourceInfo = $dscResource - LastWriteTimes = $lastWriteTimes + LastWriteTimes = $lastWriteTimes } } [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion @@ -462,12 +584,12 @@ function Invoke-DscOperation { } 'Test' { $Result = $dscResourceInstance.Test() - $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} + $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } } 'Export' { $t = $dscResourceInstance.GetType() $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null,$null) + $resultArray = $method.Invoke($null, $null) $addToActualState = $resultArray } } @@ -534,8 +656,7 @@ enum dscResourceType { Composite } -class DscResourcePropertyInfo -{ +class DscResourcePropertyInfo { [string] $Name [string] $PropertyType [bool] $IsMandatory @@ -556,4 +677,5 @@ class DscResourceInfo { [string] $ImplementedAs [string] $CompanyName [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties + [System.String[]] $Operations } From 39c57b3067855607fe8cc67ef4c02d5f78163fc2 Mon Sep 17 00:00:00 2001 From: Baby Grogu Date: Fri, 26 Jul 2024 05:31:49 +0200 Subject: [PATCH 3/7] Revert "Add class-based operation methods in DSCResourceInfo" This reverts commit 9443e1cdd619f85cdb29606093b449e65cd67c6e. --- .../psDscAdapter/psDscAdapter.psm1 | 270 +++++------------- 1 file changed, 74 insertions(+), 196 deletions(-) diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index f31f672d9..07b3763e8 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -22,19 +22,25 @@ function Import-PSDSCModule { $PSDesiredStateConfiguration = Import-Module $m -Force -PassThru } -function Get-DSCResourceModules { +function Get-DSCResourceModules +{ $listPSModuleFolders = $env:PSModulePath.Split([IO.Path]::PathSeparator) $dscModulePsd1List = [System.Collections.Generic.HashSet[System.String]]::new() - foreach ($folder in $listPSModuleFolders) { - if (!(Test-Path $folder)) { + foreach ($folder in $listPSModuleFolders) + { + if (!(Test-Path $folder)) + { continue } - foreach ($moduleFolder in Get-ChildItem $folder -Directory) { + foreach($moduleFolder in Get-ChildItem $folder -Directory) + { $addModule = $false - foreach ($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) { + foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2) + { $containsDSCResource = select-string -LiteralPath $psd1 -pattern '^[^#]*\bDscResourcesToExport\b.*' - if ($null -ne $containsDSCResource) { + if($null -ne $containsDSCResource) + { $dscModulePsd1List.Add($psd1) | Out-Null } } @@ -51,32 +57,39 @@ function Add-AstMembers { $Properties ) - foreach ($TypeConstraint in $TypeAst.BaseTypes) { - $t = $AllTypeDefinitions | Where-Object { $_.Name -eq $TypeConstraint.TypeName.Name } + foreach($TypeConstraint in $TypeAst.BaseTypes) { + $t = $AllTypeDefinitions | Where-Object {$_.Name -eq $TypeConstraint.TypeName.Name} if ($t) { Add-AstMembers $AllTypeDefinitions $t $Properties } } - foreach ($member in $TypeAst.Members) { + foreach ($member in $TypeAst.Members) + { $property = $member -as [System.Management.Automation.Language.PropertyMemberAst] - if (($property -eq $null) -or ($property.IsStatic)) { + if (($property -eq $null) -or ($property.IsStatic)) + { continue; } $skipProperty = $true $isKeyProperty = $false - foreach ($attr in $property.Attributes) { - if ($attr.TypeName.Name -eq 'DscProperty') { + foreach($attr in $property.Attributes) + { + if ($attr.TypeName.Name -eq 'DscProperty') + { $skipProperty = $false - foreach ($attrArg in $attr.NamedArguments) { - if ($attrArg.ArgumentName -eq 'Key') { + foreach($attrArg in $attr.NamedArguments) + { + if ($attrArg.ArgumentName -eq 'Key') + { $isKeyProperty = $true break } } } } - if ($skipProperty) { + if ($skipProperty) + { continue; } @@ -88,7 +101,8 @@ function Add-AstMembers { } } -function FindAndParseResourceDefinitions { +function FindAndParseResourceDefinitions +{ [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -97,11 +111,13 @@ function FindAndParseResourceDefinitions { [string]$moduleVersion ) - if (-not (Test-Path $filePath)) { + if (-not (Test-Path $filePath)) + { return } - if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) { + if (([System.IO.Path]::GetExtension($filePath) -ne ".psm1") -and ([System.IO.Path]::GetExtension($filePath) -ne ".ps1")) + { return } @@ -110,7 +126,8 @@ function FindAndParseResourceDefinitions { [System.Management.Automation.Language.Token[]] $tokens = $null [System.Management.Automation.Language.ParseError[]] $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($filePath, [ref]$tokens, [ref]$errors) - foreach ($e in $errors) { + foreach($e in $errors) + { $e | Out-String | Write-DscTrace -Operation Error } @@ -123,9 +140,12 @@ function FindAndParseResourceDefinitions { $resourceList = [System.Collections.Generic.List[DscResourceInfo]]::new() - foreach ($typeDefinitionAst in $typeDefinitions) { - foreach ($a in $typeDefinitionAst.Attributes) { - if ($a.TypeName.Name -eq 'DscResource') { + foreach($typeDefinitionAst in $typeDefinitions) + { + foreach($a in $typeDefinitionAst.Attributes) + { + if ($a.TypeName.Name -eq 'DscResource') + { $DscResourceInfo = [DscResourceInfo]::new() $DscResourceInfo.Name = $typeDefinitionAst.Name $DscResourceInfo.ResourceType = $typeDefinitionAst.Name @@ -137,10 +157,8 @@ function FindAndParseResourceDefinitions { $DscResourceInfo.ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) $DscResourceInfo.ParentPath = [System.IO.Path]::GetDirectoryName($filePath) $DscResourceInfo.Version = $moduleVersion - $DscResourceInfo.Operations = GetResourceOperationMethods -resourceName $typeDefinitionAst.Name -filePath $filePath $DscResourceInfo.Properties = [System.Collections.Generic.List[DscResourcePropertyInfo]]::new() - Add-AstMembers $typeDefinitions $typeDefinitionAst $DscResourceInfo.Properties $resourceList.Add($DscResourceInfo) @@ -151,7 +169,8 @@ function FindAndParseResourceDefinitions { return $resourceList } -function LoadPowerShellClassResourcesFromModule { +function LoadPowerShellClassResourcesFromModule +{ [CmdletBinding(HelpUri = '')] param( [Parameter(Mandatory = $true)] @@ -160,24 +179,29 @@ function LoadPowerShellClassResourcesFromModule { "Loading resources from module '$($moduleInfo.Path)'" | Write-DscTrace -Operation Trace - if ($moduleInfo.RootModule) { - if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and + if ($moduleInfo.RootModule) + { + if (([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".psm1") -and ([System.IO.Path]::GetExtension($moduleInfo.RootModule) -ne ".ps1") -and - (-not $z.NestedModules)) { + (-not $z.NestedModules)) + { "RootModule is neither psm1 nor ps1 '$($moduleInfo.RootModule)'" | Write-DscTrace -Operation Trace return [System.Collections.Generic.List[DscResourceInfo]]::new() } $scriptPath = Join-Path $moduleInfo.ModuleBase $moduleInfo.RootModule } - else { + else + { $scriptPath = $moduleInfo.Path; } $Resources = FindAndParseResourceDefinitions $scriptPath $moduleInfo.Version - if ($moduleInfo.NestedModules) { - foreach ($nestedModule in $moduleInfo.NestedModules) { + if ($moduleInfo.NestedModules) + { + foreach ($nestedModule in $moduleInfo.NestedModules) + { $resourcesOfNestedModules = LoadPowerShellClassResourcesFromModule $nestedModule if ($resourcesOfNestedModules) { $Resources.AddRange($resourcesOfNestedModules) @@ -188,153 +212,6 @@ function LoadPowerShellClassResourcesFromModule { return $Resources } -function GetResourceOperationMethods { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $resourceName, - - [Parameter(Mandatory = $true)] - [string] $filePath - ) - - # dot source scope - try { - . (LoadClassAndEnumsFromModuleFile -filePath $filePath) - } catch { - ("Module: '{0}' not loaded for resource operation discovery."-f $filePath) | Write-DscTrace - } - - $inputObject = ReturnTypeNameObject -TypeName $resourceName - - if (-not $inputObject) { - return @( - 'Get', - 'Test', - 'Set' - ) - } - - # TODO: There might be more properties available - $knownMemberTypes = @('Equals', 'GetHashCode', 'GetType', 'ToString') - return ($inputObject | Get-Member | Where-Object { $_.MemberType -eq 'Method' -and $_.Name -notin $knownMemberTypes }).Name -} - -function ReturnTypeNameObject { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $TypeName - ) - - try { - $inputObject = New-Object -TypeName $TypeName -ErrorAction Stop - } - catch { - "Could not create: $TypeName" | Write-DscTrace - } - - return $inputObject -} - -function LoadClassAndEnumsFromModuleFile { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [string] $filePath - ) - - if (-not (Test-Path $filePath -ErrorAction SilentlyContinue)) { - return - } - - $ctx = Get-Content $filePath - - $string = @( - 'using namespace System.Collections.Generic', # TODO: Figure away out to get using statements included - (GetEnumCodeBlock -Content $ctx), - (GetClassCodeBlock -Content $ctx) - ) - - # TODO: Might have to do something with the path - $outPath = Join-Path -Path $env:TEMP -ChildPath ("{0}.ps1" -f [System.Guid]::NewGuid().Guid) - $string | Out-File -FilePath $outPath - - return $outPath -} - -function GetClassCodeBlock { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content - ) - - $ctx = $Content - - $lines = ($ctx | Select-String -Pattern '\[DSCResource\(\)]').LineNumber - if ($lines.Count -eq 0 ) { - return - } - - $lastLineNumber = $lines[-1] - $index = 1 - # Bring all class strings together after the last one - $classStrings = foreach ($line in $lines) { - if ($line -eq $lastLineNumber) { - $lastModuleLine = $ctx.Length - - $line = $line - 1 - $block = $ctx[$line..$lastModuleLine] - $block - break - } - - $line = $line - 1 - $curlyBracketLine = FindCurlyBracket -Content $ctx -LineNumber $lines[$index] - $block = $ctx[$line..$curlyBracketLine] - - $index++ - $block - } - - return $classStrings -} - -function GetEnumCodeBlock { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content - ) - - # Build regex to catch enum blocks - $regex = [regex]::new('enum\s+(\w+)\s*\{([^}]+)\}') - - $hits = $regex.Matches($Content) - - # return as single lines - return ($hits.Value -Split " ") -} - -function FindCurlyBracket { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [array] $Content, - - [Parameter(Mandatory = $true)] - [int] $LineNumber - ) - do { - if ($Content[$LineNumber] -eq "}") { - return $LineNumber - } - - $LineNumber-- - } while ($LineNumber -ne 0) -} - <# public function Invoke-DscCacheRefresh .SYNOPSIS This function caches the results of the Get-DscResource call to optimize performance. @@ -360,8 +237,7 @@ function Invoke-DscCacheRefresh { $cacheFilePath = if ($IsWindows) { # PS 6+ on Windows Join-Path $env:LocalAppData "dsc\PSAdapterCache.json" - } - else { + } else { # PS 6+ on Linux/Mac Join-Path $env:HOME ".dsc" "PSAdapterCache.json" } @@ -373,9 +249,8 @@ function Invoke-DscCacheRefresh { if ($cache.CacheSchemaVersion -ne $script:CurrentCacheSchemaVersion) { $refreshCache = $true - "Incompatible version of cache in file '" + $cache.CacheSchemaVersion + "' (expected '" + $script:CurrentCacheSchemaVersion + "')" | Write-DscTrace - } - else { + "Incompatible version of cache in file '"+$cache.CacheSchemaVersion+"' (expected '"+$script:CurrentCacheSchemaVersion+"')" | Write-DscTrace + } else { $dscResourceCacheEntries = $cache.ResourceCache if ($dscResourceCacheEntries.Count -eq 0) { @@ -384,7 +259,8 @@ function Invoke-DscCacheRefresh { "Filtered DscResourceCache cache is empty" | Write-DscTrace } - else { + else + { "Checking cache for stale entries" | Write-DscTrace foreach ($cacheEntry in $dscResourceCacheEntries) { @@ -392,19 +268,20 @@ function Invoke-DscCacheRefresh { $cacheEntry.LastWriteTimes.PSObject.Properties | ForEach-Object { - if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) { + if (-not ((Get-Item $_.Name).LastWriteTime.Equals([DateTime]$_.Value))) + { "Detected stale cache entry '$($_.Name)'" | Write-DscTrace $refreshCache = $true break } } - if ($refreshCache) { break } + if ($refreshCache) {break} } "Checking cache for stale PSModulePath" | Write-DscTrace - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} $hs_cache = [System.Collections.Generic.HashSet[string]]($cache.PSModulePaths) $hs_live = [System.Collections.Generic.HashSet[string]]($m.FullName) @@ -432,10 +309,11 @@ function Invoke-DscCacheRefresh { $DscResources = [System.Collections.Generic.List[DscResourceInfo]]::new() $dscResourceModulePsd1s = Get-DSCResourceModules - if ($null -ne $dscResourceModulePsd1s) { + if($null -ne $dscResourceModulePsd1s) { $modules = Get-Module -ListAvailable -Name ($dscResourceModulePsd1s) $processedModuleNames = @{} - foreach ($mod in $modules) { + foreach ($mod in $modules) + { if (-not ($processedModuleNames.ContainsKey($mod.Name))) { $processedModuleNames.Add($mod.Name, $true) @@ -459,20 +337,20 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*psm1", "*.mof" -ea Ignore | % { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1","*.psd1","*psm1","*.mof" -ea Ignore | % { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } $dscResourceCacheEntries += [dscResourceCacheEntry]@{ Type = "$moduleName/$($dscResource.Name)" DscResourceInfo = $dscResource - LastWriteTimes = $lastWriteTimes + LastWriteTimes = $lastWriteTimes } } [dscResourceCache]$cache = [dscResourceCache]::new() $cache.ResourceCache = $dscResourceCacheEntries - $m = $env:PSModulePath -split [IO.Path]::PathSeparator | % { Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue } + $m = $env:PSModulePath -split [IO.Path]::PathSeparator | %{Get-ChildItem -Directory -Path $_ -Depth 1 -ea SilentlyContinue} $cache.PSModulePaths = $m.FullName $cache.CacheSchemaVersion = $script:CurrentCacheSchemaVersion @@ -584,12 +462,12 @@ function Invoke-DscOperation { } 'Test' { $Result = $dscResourceInstance.Test() - $addToActualState.properties = [psobject]@{'InDesiredState' = $Result } + $addToActualState.properties = [psobject]@{'InDesiredState'=$Result} } 'Export' { $t = $dscResourceInstance.GetType() $method = $t.GetMethod('Export') - $resultArray = $method.Invoke($null, $null) + $resultArray = $method.Invoke($null,$null) $addToActualState = $resultArray } } @@ -656,7 +534,8 @@ enum dscResourceType { Composite } -class DscResourcePropertyInfo { +class DscResourcePropertyInfo +{ [string] $Name [string] $PropertyType [bool] $IsMandatory @@ -677,5 +556,4 @@ class DscResourceInfo { [string] $ImplementedAs [string] $CompanyName [System.Collections.Generic.List[DscResourcePropertyInfo]] $Properties - [System.String[]] $Operations } From aad9f3b4bb1b42abee7623538417fa2be940ff96 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Thu, 15 Aug 2024 16:45:34 +0200 Subject: [PATCH 4/7] Start working on WMI adapter improvements --- wmi-adapter/Tests/wmi.tests.ps1 | 77 +++--- wmi-adapter/copy_files.txt | 4 +- wmi-adapter/wmi.dsc.resource.json | 98 +++++--- wmi-adapter/wmi.resource.ps1 | 236 +++++++++--------- wmi-adapter/wmiAdapter.psd1 | 47 ++++ wmi-adapter/wmiAdapter.psm1 | 384 ++++++++++++++++++++++++++++++ 6 files changed, 663 insertions(+), 183 deletions(-) create mode 100644 wmi-adapter/wmiAdapter.psd1 create mode 100644 wmi-adapter/wmiAdapter.psm1 diff --git a/wmi-adapter/Tests/wmi.tests.ps1 b/wmi-adapter/Tests/wmi.tests.ps1 index e5d2115c0..a96f7d47d 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,58 @@ 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.value.LastBootUpTime | Should -Not -BeNull + $res.results.result.actualstate.result[0].properties.value.Caption | Should -Not -BeNull + $res.results.result.actualstate.result[0].properties.value.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.value.Model | Should -Not -BeNullOrEmpty + $res.results.result.actualstate.result[0].properties.value.Description | Should -Not -BeNullOrEmpty + } } - It 'Get works on a config with WMI resources' -Skip:(!$IsWindows){ + # TODO: work on set test + # Context "Set WMI resources" { + # } - $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 - } - - 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 - } + # TODO: get export working + # 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[0].properties.result.count | Should -BeGreaterThan 1 + # $json.resources[0].properties.result.properties.value | Should -not -BeNullOrEmpty + # } + # } } 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 5b63c7d37..0d1bb66ac 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 6eb2f88d9..6d7fae39d 100644 --- a/wmi-adapter/wmi.resource.ps1 +++ b/wmi-adapter/wmi.resource.ps1 @@ -3,142 +3,150 @@ [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 += [PSCustomObject]@{ + name = $actualstate.name + type = $actualstate.type + properties = $actualstate.properties + } } - } - $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 000000000..4dd03d3db --- /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 000000000..9e4ebd716 --- /dev/null +++ b/wmi-adapter/wmiAdapter.psm1 @@ -0,0 +1,384 @@ +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 + } + } + + # 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 + $instance_result = $r.properties + + # return the Microsoft.Management.Infrastructure.CimInstance class + $addToActualState.CimInstance = $wmi_instance + } + 'Test' + { + # TODO: implement test + } + # TODO: validate output + # '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' + { + # TODO: validate output + $setState = GetCimSpace -Operation $Operation -DesiredState $DesiredState + + $wmiResources = ValidateCimMethodAndArguments -DesiredState $setState + + foreach ($resource in $wmiResources) + { + $addToActualState = InvokeCimMethod @resource + } + } + '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 From a018373329f28a6b155daba70919d3c8a2cd059d Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Thu, 15 Aug 2024 16:47:51 +0200 Subject: [PATCH 5/7] Remove helpers --- package-lock.json | 6 + powershell-helpers/README.md | 13 - powershell-helpers/dscCfgMigMod.psd1 | 47 --- powershell-helpers/dscCfgMigMod.psm1 | 384 ------------------ .../tests/dscCfgMigMod.tests.ps1 | 24 -- 5 files changed, 6 insertions(+), 468 deletions(-) create mode 100644 package-lock.json delete mode 100644 powershell-helpers/README.md delete mode 100644 powershell-helpers/dscCfgMigMod.psd1 delete mode 100644 powershell-helpers/dscCfgMigMod.psm1 delete mode 100644 powershell-helpers/tests/dscCfgMigMod.tests.ps1 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..37188fe4a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "operation-methods", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/powershell-helpers/README.md b/powershell-helpers/README.md deleted file mode 100644 index d48f4e871..000000000 --- a/powershell-helpers/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Introduction - -The `powershell-adapters` folder contains helper modules that can be loaded into your PowerShell session to assist you in familiarizing yourself with new DSC concepts. To see the availability of helper modules, see the following list: - -- **DSC Configuration Migration Module**: - Aids in the assistance of grabbing configuration documents written in PowerShell code and transform them to valid configuration documents for the DSC version 3 core engine (e.g. YAML or JSON). - -## Getting started - -To get started using the helper modules, you can follow the below steps. This example uses the _DSC Configuration Migration Tool_ to be loaded into the session: - -1. Open a PowerShell terminal session -2. Execute the following command: `Import-Module "powershell-helpers\dscConfigurationMigrationTool.psm1"` -3. Discover examples using: `Get-Help ConvertTo-DscYaml` diff --git a/powershell-helpers/dscCfgMigMod.psd1 b/powershell-helpers/dscCfgMigMod.psd1 deleted file mode 100644 index 83d1ac091..000000000 --- a/powershell-helpers/dscCfgMigMod.psd1 +++ /dev/null @@ -1,47 +0,0 @@ -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'dscCfgMigMod.psm1' - - # Version number of this module. - moduleVersion = '0.0.1' - - # ID used to uniquely identify this module - GUID = '42bf8cb0-210c-4dac-8614-319d9287c6dc' - - # 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 Migration Module helper' - - # Modules that must be imported into the global environment prior to importing this module - RequiredModules = @('powershell-yaml') - - # 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 = @( - 'ConvertTo-DscJson' - 'ConvertTo-DscYaml' - ) - - # 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' - } - } -} diff --git a/powershell-helpers/dscCfgMigMod.psm1 b/powershell-helpers/dscCfgMigMod.psm1 deleted file mode 100644 index c61ff4bff..000000000 --- a/powershell-helpers/dscCfgMigMod.psm1 +++ /dev/null @@ -1,384 +0,0 @@ -#region Main functions -function ConvertTo-DscJson -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 JSON format. - - .DESCRIPTION - The function ConvertTo-DscJson converts a PowerShell DSC configuration document to DSC version 3 JSON format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscJson -Path $Path - - Returns: - { - "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json", - "resources": { - "name": "TestResource", - "type": "Microsoft.DSC/PowerShell", - "properties": { - "resources": [ - { - "name": "Configure test resource", - "type": "TestResource/TestResource", - "properties": { - "Name": "MyTestResource", - "Ensure": "Absent" - } - } - ] - } - } - } - - .NOTES - Tags: DSC, Migration, JSON - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} - -function ConvertTo-DscYaml -{ - <# - .SYNOPSIS - Convert a PowerShell DSC configuration document to DSC version 3 YAML format. - - .DESCRIPTION - The function ConvertTo-DscYaml converts a PowerShell DSC configuration document to DSC version 3 YAML format from a path. - - .PARAMETER Path - The path to valid PowerShell DSC configuration document - - .EXAMPLE - PS C:\> $configuration = @' - Configuration TestResource { - Import-DscResource -ModuleName TestResource - Node localhost { - TestResource 'Configure test resource' { - Ensure = 'Absent' - Name = 'MyTestResource' - } - } - } - '@ - PS C:\> $Path = Join-Path -Path $env:TEMP -ChildPath 'configuration.ps1' - PS C:\> $configuration | Out-File -FilePath $Path - PS C:\> ConvertTo-DscYaml -Path $Path - - Returns: - $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json - resources: - name: TestResource - type: Microsoft.DSC/PowerShell - properties: - resources: - - name: Configure test resource - type: TestResource/TestResource - properties: - Name: MyTestResource - Ensure: Absent - - .NOTES - Tags: DSC, Migration, YAML - #> - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - begin - { - Write-Verbose ("Starting: {0}" -f $MyInvocation.MyCommand.Name) - } - - process - { - $inputObject = BuildConfigurationDocument -Path $Path -Format YAML - } - end - { - Write-Verbose ("Ended: {0}" -f $MyInvocation.MyCommand.Name) - return $inputObject - } -} -#endRegion Main functions - -#region Helper functions -function FindAndExtractConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - if (-not (TestPathExtension $Path)) - { - return @{} - } - - # Parse the abstract syntax tree to get all hash table values representing the configuration resources - [System.Management.Automation.Language.Token[]] $tokens = $null - [System.Management.Automation.Language.ParseError[]] $errors = $null - $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) - $configurations = $ast.FindAll({$args[0].GetType().Name -like 'HashtableAst'}, $true) - - # Create configuration document resource class (can be re-used) - $configurationDocument = [DscConfigurationResource]::new() - - # Build simple regex - $regex = [regex]::new('Configuration\s+(\w+)') - $configValue = $regex.Matches($ast.Extent.Text).Value - - if (-not $configValue) - { - return - } - - $documentConfigurationName = $configValue.TrimStart('Configuration').Trim(" ") - - # Start to build the outer basic format - $configurationDocument.name = $documentConfigurationName - $configurationDocument.type = 'Microsoft.DSC/PowerShell' # TODO: Add functions later to valid the adapter type - - # Bag to hold resources - $resourceProps = [System.Collections.Generic.List[object]]::new() - - foreach ($configuration in $configurations) - { - # Get parent configuration details - $resourceName = ($configuration.Parent.CommandElements.Value | Select-Object -Last 1 ) - $resourceConfigurationName = ($configuration.Parent.CommandElements.Value | Select-Object -First 1) - - # Get module details - $module = Get-DscResource -Name $resourceConfigurationName -ErrorAction SilentlyContinue - - # Build the module - $resource = [DscConfigurationResource]::new() - $resource.properties = $configuration.SafeGetValue() - $resource.name = $resourceName - $resource.type = ("{0}/{1}" -f $module.ModuleName, $resourceConfigurationName) - # TODO: Might have to change because it takes time. If there is only one Import-DscResource statement, we can simply RegEx it out, else use Get-DscResource - # $document.ModuleName = $module.ModuleName - - Write-Verbose ("Adding document with data") - Write-Verbose ($resource | ConvertTo-Json | Out-String) - $resourceProps.Add($resource) - } - - # Add all the resources - $configurationDocument.properties = @{ - resources = $resourceProps - } - - return $configurationDocument -} - -function BuildConfigurationDocument -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path, - - [ValidateSet('JSON', 'YAML')] - [System.String] - $Format = 'JSON' - ) - - $configurationDocument = [ordered]@{ - "`$schema" = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/config/document.json" # TODO: Figure out how to extract latest document.json from schemas folder - resources = FindAndExtractConfigurationDocument -Path $Path - } - - switch ($Format) - { - "JSON" { - $inputObject = ($configurationDocument | ConvertTo-Json -Depth 10) - } - "YAML" { - if (TestYamlModule) - { - $inputObject = ($configurationDocument | ConvertTo-Yaml) - } - else - { - $inputObject = @{} - } - } - default { - $inputObject = $configurationDocument - } - } - - return $inputObject -} - -function TestPathExtension -{ - [CmdletBinding()] - Param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Path - ) - - $res = $true - - if (-not (Test-Path $Path)) - { - $res = $false - } - - if (([System.IO.Path]::GetExtension($Path) -ne ".ps1")) - { - $res = $false - } - - return $res -} - -function TestYamlModule -{ - if (-not (Get-Command -Name 'ConvertTo-Yaml' -ErrorAction SilentlyContinue)) - { - return $false - } - - return $true -} - -function GetPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\Modules", - "$env:ProgramFiles\PowerShell\7\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function GetWindowsPowerShellPath -{ - param - ( - $Path - ) - - $knownPath = @( - "$env:USERPROFILE\Documents\WindowsPowerShell\Modules", - "$env:ProgramFiles\WindowsPowerShell\Modules", - "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules" - ) - - foreach ($known in $knownPath) - { - if ($Path.StartsWith($known)) - { - return $true - } - } - - return $false -} - -function ResolvePowerShellPath -{ - [CmdletBinding()] - Param - ( - [System.String] - $Path - ) - - if (-not (Test-Path $Path)) - { - return - } - - if (([System.IO.Path]::GetExtension($Path) -ne ".psm1")) - { - return - } - - if (GetPowerShellPath -Path $Path) - { - return "Microsoft.DSC/PowerShell" - } - - if (GetWindowsPowerShellPath -Path $Path) - { - return "Microsoft.Windows/WindowsPowerShell" - } - - return $null # TODO: Or default Microsoft.DSC/PowerShell -} - -#endRegion Helper functions - -#region Classes -class DscConfigurationResource -{ - [string] $name - [string] $type - [hashtable] $properties -} -#endRegion classes \ No newline at end of file diff --git a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 b/powershell-helpers/tests/dscCfgMigMod.tests.ps1 deleted file mode 100644 index b966993ad..000000000 --- a/powershell-helpers/tests/dscCfgMigMod.tests.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -Describe "DSC Configuration Migration Module tests" { - BeforeAll { - $modPath = (Resolve-Path -Path "$PSScriptRoot\..\dscCfgMigMod.psd1").Path - $modLoad = Import-Module $modPath -Force -PassThru - } - - Context "ConvertTo-DscYaml" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscYaml -Path 'idonotexist' | ConvertFrom-Yaml) - $res.resources | Should -BeNullOrEmpty - } - } - - Context "ConvertTo-DscJson" { - It "Should create an empty resource block" { - $res = (ConvertTo-DscJson -Path 'idonotexist' | ConvertFrom-Json) - $res.resources | Should -BeNullOrEmpty - } - } - - AfterAll { - Remove-Module -Name $modLoad.Name -Force - } -} From caf32f6fb4314124b3cdab15e32aa4f60810e6e3 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Thu, 15 Aug 2024 16:48:50 +0200 Subject: [PATCH 6/7] Remove package.json --- package-lock.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 37188fe4a..000000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "operation-methods", - "lockfileVersion": 2, - "requires": true, - "packages": {} -} From 0cfca099c97706a9849fcceab3d6d409d16d2385 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Fri, 16 Aug 2024 07:49:11 +0200 Subject: [PATCH 7/7] Added new Pester test with Export operation enabled --- wmi-adapter/Tests/wmi.tests.ps1 | 70 ++++++++++++++++++++++++--------- wmi-adapter/wmi.resource.ps1 | 7 +--- wmi-adapter/wmiAdapter.psm1 | 49 +++++++++++++---------- 3 files changed, 82 insertions(+), 44 deletions(-) diff --git a/wmi-adapter/Tests/wmi.tests.ps1 b/wmi-adapter/Tests/wmi.tests.ps1 index a96f7d47d..7c479560a 100644 --- a/wmi-adapter/Tests/wmi.tests.ps1 +++ b/wmi-adapter/Tests/wmi.tests.ps1 @@ -43,9 +43,9 @@ Describe 'WMI adapter resource tests' { $r = Get-Content -Raw $configPath | dsc config get $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.results.result.actualstate.result[0].properties.value.LastBootUpTime | Should -Not -BeNull - $res.results.result.actualstate.result[0].properties.value.Caption | Should -Not -BeNull - $res.results.result.actualstate.result[0].properties.value.NumberOfProcesses | Should -Not -BeNull + $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) { @@ -54,23 +54,57 @@ Describe 'WMI adapter resource tests' { $LASTEXITCODE | Should -Be 0 $r | Should -Not -BeNullOrEmpty $res = $r | ConvertFrom-Json - $res.results.result.actualstate.result[0].properties.value.Model | Should -Not -BeNullOrEmpty - $res.results.result.actualstate.result[0].properties.value.Description | Should -Not -BeNullOrEmpty + $res.results.result.actualstate.result[0].properties.Model | Should -Not -BeNullOrEmpty + $res.results.result.actualstate.result[0].properties.Description | Should -Not -BeNullOrEmpty } } - # TODO: work on set test - # Context "Set WMI resources" { - # } + # 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 - # TODO: get export working - # 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[0].properties.result.count | Should -BeGreaterThan 1 - # $json.resources[0].properties.result.properties.value | Should -not -BeNullOrEmpty - # } - # } + # run the creation of process + $r = ($inputs | ConvertTo-Json -Compress) | dsc resource set -r root.cimv2/Win32_Process + + # 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 + } + } + + 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/wmi.resource.ps1 b/wmi-adapter/wmi.resource.ps1 index 6d7fae39d..504b6277c 100644 --- a/wmi-adapter/wmi.resource.ps1 +++ b/wmi-adapter/wmi.resource.ps1 @@ -106,6 +106,7 @@ switch ($Operation) } { @('Get', 'Set', 'Test', 'Export') -contains $_ } { + $desiredState = $wmiAdapter.invoke( { param($jsonInput) Get-DscResourceObject -jsonInput $jsonInput }, $jsonInput ) if ($null -eq $desiredState) { @@ -125,11 +126,7 @@ switch ($Operation) exit 1 } - $result += [PSCustomObject]@{ - name = $actualstate.name - type = $actualstate.type - properties = $actualstate.properties - } + $result += $actualstate } # OUTPUT json to stderr for debug, and to stdout diff --git a/wmi-adapter/wmiAdapter.psm1 b/wmi-adapter/wmiAdapter.psm1 index 9e4ebd716..2498b01a4 100644 --- a/wmi-adapter/wmiAdapter.psm1 +++ b/wmi-adapter/wmiAdapter.psm1 @@ -109,6 +109,8 @@ function GetCimSpace } } + $addToActualState.properties = $instance_result + # TODO: validate if we can set it to null $addToActualState.CimInstance = $null } @@ -118,30 +120,30 @@ function GetCimSpace $wmi_instance = $wmi_instances[0] # add the properties from INPUT - $instance_result = $r.properties + $addToActualState.properties = $r.properties # return the Microsoft.Management.Infrastructure.CimInstance class $addToActualState.CimInstance = $wmi_instance + } 'Test' { # TODO: implement test } - # TODO: validate output - # '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) - # } - # } + '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 @@ -254,14 +256,19 @@ function Invoke-DscWmi } 'Set' { - # TODO: validate output - $setState = GetCimSpace -Operation $Operation -DesiredState $DesiredState - - $wmiResources = ValidateCimMethodAndArguments -DesiredState $setState + $addToActualState = GetCimSpace -Operation $Operation -DesiredState $DesiredState + $wmiResources = ValidateCimMethodAndArguments -DesiredState $addToActualState foreach ($resource in $wmiResources) { - $addToActualState = InvokeCimMethod @resource + $null = InvokeCimMethod @resource + } + + # reset the value to be empty + $addToActualState = [PSCustomObject]@{ + name = $addToActualState.name + type = $addToActualState.type + properties = $null } } 'Test'