Write-DSCResource.ps1
function Write-DSCResource { <# .Synopsis Automatically generates the tedious code required to implement a DSC resource .Description Automatically generates the source code for a DSC resource. The DSC resource can be generated with a simple hashtable and a set of scriptblocks, or it can be implemented by wrapping one or more commands. .Example Write-DSCResource -Property @{ OutputPath = "The output path" Base64Content = "The file contents, in base64" } -Test { if (-not [IO.File]::Exists($OutputPath)) { return $false } $b64 = [Convert]::FromBase64String($Base64Content) $md5 = [Security.Cryptography.MD5]::Create() $hash1 = [BitConverter]::ToString($md5.ComputeHash($b64)) $hash2 = [BitConverter]::ToString($md5.ComputeHash([IO.File]::ReadAllBytes($OutputPath))) $hash1 -eq $hash2 } -Get { @{ OutputPath = $outputPath Base64Content = try { [Convert]::ToBase64String([IO.File]::ReadAllBytes($outputPath)) } catch { $null } } } -Set { if (-not (Test-Path $outputPath)) { $nf = New-Item -Path $outputPath -Force -ItemType File } [IO.File]::WriteAllBytes($outputPath, [Convert]::FromBase64String($base64Content)) } -KeyProperty OutputPath .Link Initialize-DSCResource .Link Reset-DSCResource #> [CmdletBinding(DefaultParameterSetName='PropertyHashtable')] [OutputType([Nullable])] param( # The name of the resource [Parameter(Mandatory=$true,Position=0)] [string] $ResourceName, # The root directory of the module in which the DSC resource will be contained. [Parameter(Position=1)] [string] $ModuleRoot, # A hashtable describing the properties of the DSC resource. The keys are the names of the properties, and the values are a description, with an optional type [Parameter(Mandatory=$true, ParameterSetName='PropertyHashtable', Position=3)] [ValidateScript({ foreach ($kv in $_.GetEnumerator()) { if ($kv.Key -isnot [string]) { throw "$($kv.Key) must be a string" } if ($kv.Value -isnot [string]){ if ($kv.Value -isnot [Object[]]) { throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information" } $description, $type, $RestOfInfo = $kv.Value if ($description -isnot [string]) { throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information" } if (-not ($type -as [type])) { throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information" } if ($restOfInfo -and (-not ($RestOfInfo -as [Hashtable]))) { throw "$($kv.Key) must either be a string, a string and a type, or a string, a type, and a hashtable of information" } } } return $true })] [Hashtable] $Property, # The names of the key properties. [Parameter(Mandatory=$true,ParameterSetName='PropertyHashtable',Position=2)] [Parameter(Mandatory=$true,ParameterSetName='WrapCommand',Position=2)] [string[]] $KeyProperty, # A list of read only properties [string[]] $ReadOnlyProperty, # A List of mandatory properties. These properties, while not used as a key, will be required for the get/test/set functions in the DSC resource. [Alias('RequiredProperty', 'MandatoryProperties', 'RequiredProperties')] [string[]] $MandatoryProperty, # A hashtable containing the names of properties and their potential values [Parameter(ValueFromPipelineByPropertyName=$true)] [ValidateScript({ foreach ($kv in $_.GetEnumerator()) { if (-not ($kv.Value -as [string[]]) -and -not ($kv.Value -as [Hashtable])) { throw "$($kv.Key) must be a list of valid values or a Hashtable" } } return $true })] [Hashtable] $PropertyValueMap = @{}, # A hashtable of containing valid values or enumerations for a particular property. The key is the name of the property. If the value is a list, the list will be the acceptable values for that property. If the value is a hashtable, the keys will be friendly values, and the values will be the underlying system value. [Parameter(ValueFromPipelineByPropertyName=$true)] [Hashtable] $ValueMap, # The underlying implementation of Get-TargetResource. By default, this returns an empty hashtable. [ScriptBlock] $Get = {@{}}, # The underlying implementation of Set-TargetResource. By default, this does nothing. [ScriptBlock] $Set = {}, # The underlying implementation of Test-TargetResource. By default, this returns $false. [ScriptBlock] $Test = {return $false}, # The version of the DSC resource. By default, 1.0. [Version] $Version = '1.0', # The Command that will be called when the resource is set. The Set Command is the only command required to wrap a DSC resource. The Test Command must use the same parameter names [Parameter(Mandatory=$true,ParameterSetName='WrapCommand',ValueFromPipeline=$true)] [Management.Automation.CommandInfo] $SetCommand, # The Command that will be called when the resource is requested. Output from this command will be converted into PowerShell hashtables. [Parameter(ParameterSetName='WrapCommand')] [Management.Automation.CommandInfo] $GetCommand, # The Test Command. A non-null or empty output from this command will be considered a pass [Parameter(ParameterSetName='WrapCommand')] [Management.Automation.CommandInfo] $TestCommand, # Default parameters that will be passed to the set command [Parameter(ParameterSetName='WrapCommand')] [Hashtable] $SetDefaultParameter = @{}, # Default parameters that will be passed to the get command. [Parameter(ParameterSetName='WrapCommand')] [Hashtable] $GetDefaultParameter = @{}, # Default parameters that will be passed to the test command. [Parameter(ParameterSetName='WrapCommand')] [Hashtable] $TestDefaultParameter = @{}, # A list of parameters to be excluded from the Set command. [Parameter(ParameterSetName='WrapCommand')] [string[]] $SetParameterBlacklist, # A list of parameters to be excluded from the Test command. If the parameter is not in the Set blacklist, it will exist as a DSC resource setting but will be ignored when the underlying Test command is called. [Parameter(ParameterSetName='WrapCommand')] [string[]] $TestParameterBlackList, # A list of parameters to be excluded from the Get command. If the parameter is not in the Set blacklist, it will exist as a DSC resource setting but will be ignored when the underlying Get command is called. [Parameter(ParameterSetName='WrapCommand')] [string[]] $GetParameterBlackList, # A map of DSC resource setting names to the underlying parameters in the Set function [Parameter(ParameterSetName='WrapCommand')] [Hashtable] $SetParameterNameMap = @{}, # A ScriptBlock used to interpret the results of the test command. The variable $testResult will contain the results of the test command. If no script block is provided, any output from the Test command will be converted to a boolean. No output, or an explicit output of false, will fail the test. [Parameter(ParameterSetName='WrapCommand')] [ScriptBlock] $ProcessTestCommandResult) begin { $myCmd = $MyInvocation.MyCommand #region Type Lookup Tables $CimTypeMap = @{ [SByte]="Sint8" [String]="String" [Byte[]]="Uint8[]" [Single]="Real32" [Int32]="Sint32" [PSCredential]="MSFT_Credential" [PSCredential[]]="MSFT_Credential[]" [UInt64[]]="Uint64[]" [UInt64]="Uint64" [Char]="Char16" [Hashtable] = "Microsoft.Management.Infrastructure.CimInstance[]" [String[]]="String[]" [Double]="Real64" [Double[]]="Real64[]" [DateTime]="DateTime" [Byte]="Uint8" [Char[]]="Char16[]" [Int32[]]="Sint32[]" [Boolean]="Boolean" [Uint32]="Uint32" [Uint32[]]="Uint32[]" [Int16[]]="Sint16[]" [Single[]]="Real32[]" [UInt16]="Uint16" [Int64[]]="Sint64[]" [UInt16[]]="Uint16[]" [Int64]="Sint64" [SByte[]]="Sint8[]" [Boolean[]]="Boolean[]" [Int16]="Sint16" [DateTime[]]="DateTime[]" [Timespan] = "String" [Switch] = "Boolean" [ScriptBlock] = "String" [ScriptBlock[]] = "String[]" [TimeSpan[]] = "String[]" } $EmbeddedInstances = @{ [Hashtable] = "MSFT_KeyValuePair" [PSCredential] = "MSFT_Credential" [Hashtable[]] = "MSFT_KeyValuePair" [PSCredential[]] = "MSFT_Credential" } #endregion Type Lookup Tables } process { if ($PSCmdlet.ParameterSetName -eq 'WrapCommand') { $Splat = @{ ResourceName=$ResourceName ModuleRoot=$ModuleRoot KeyProperty = $KeyProperty Version = $Version } $boundParams = @{} + $PSBoundParameters $paramNames = @{} $paramTypes = @{} $setCmdHelp = $setCommand | Get-Help foreach ($param in $setCmdHelp.Parameters.parameter) { if ($SetParameterBlacklist -contains $param.Name) { Write-Verbose "Skipping $($param.name) because it was blacklisted" continue } if (-not $Splat.Property) { $splat.Property = @{} } $parameterType = ($param.type.name -as [type]) if (-not $parameterType -and $param.type.name -eq 'SwitchParameter') { $parameterType = [switch] } if ($CimTypeMap.Keys -notcontains $parameterType) { Write-Verbose "Skipping $($param.name) because $parameterType is not supported by CIM" continue } $parameterName = if ($SetParameterNameMap.ContainsKey($param.Name)) { $SetParameterNameMap[$param.Name] } else { $param.Name } if (-not $parameterName) { continue } $splat.Property.($Param.Name) = @() $splat.Property.($Param.Name) += @($param.description | Select-Object -ExpandProperty Text) -join ([Environment]::NewLine) $splat.Property.($Param.Name) += $parameterType $paramNames += @{$parameterName = $param.Name} $paramTypes += @{$parameterName = $parameterType } } $null = $null $setScript = @" `$splat = @{} `$setCommand = `$executionContext.SessionState.InvokeCommand.GetCommand('$SetCommand', 'all') $(@(foreach ($kv in $paramNames.GetEnumerator()) { $inlineValue = if ($setcommand.Parameters[$kv.Value].ParameterType -eq [ScriptBlock]) { "[ScriptBlock]::Create(`$$($kv.Key))" } elseif ($setcommand.Parameters[$kv.Value].ParameterType -eq [ScriptBlock[]]) { "foreach (`$sb in `$$($kv.Key)) { [ScriptBlock]::Create(`$sb) } " } else { "`$$($kv.Key)" } $null = $null "if (`$psBoundParameters.ContainsKey('$($kv.Key)')) { `$splat['$($kv.Value)'] = $inlineValue }"}) -join ([Environment]::NewLine)) $( if ($SetParameterBlacklist) { @(foreach ($p in $SetParameterBlacklist) { "`$splat.Remove('$p')" }) -join ([Environment]::NewLine) }) $(@(foreach ($default in $SetDefaultParameter.GetEnumerator()) { "if (-not `$splat.ContainsKey('$($default.Key)')) { `$splat.$($default.Key) = $( if ($default.Value -is [string]) { "'$($default.Value)'" } elseif ($default.Value -is [Bool]) { "`$$($default.Value)" } else { "$($default.Value)" }) }"}) -join ([Environment]::NewLine)) $SetCommand @splat | Out-Null "@ $setscriptBlock = [ScriptBlock]::Create($setScript) if ("$Set") { $Splat.Set = $Set } elseif ($setscriptBlock) { $splat.Set = $setscriptBlock } if ("$get") { $Splat.Get = $Get } elseif ($GetCommand) { $GetScript = @" `$splat = @{} $(@(foreach ($kv in $paramNames.GetEnumerator()) { "if (`$psBoundParameters.ContainsKey('$($kv.Key)')) { `$splat['$($kv.Value)'] = $(if ($setcommand.Parameters[$kv.Value].ParameterType -like "*Automation.ScriptBlock*") {"[ScriptBlock]::Create(`$$($kv.Key))"} else {"`$$($kv.Key)" }) }"}) -join ([Environment]::NewLine)) $( foreach ($p in $getParameterBlackList) { "`$splat.Remove('$p') " }) $(foreach ($default in $GetDefaultParameter.GetEnumerator()) { " if (-not `$splat.ContainsKey('$($default.Key)')) { `$splat.$($default.Key) = $( if ($default.Value -is [string]) { "'$($default.Value)'" } elseif ($default.Value -is [Bool]) { "`$$($default.Value)" } else { "$($default.Value)" })" }) `$getResults = $GetCommand @splat `$getResultsHT = @{} if (`$getResults -is [Hashtable]) { `$getResultsHT = `$getResults } elseif (`$getResults) { foreach (`$prop in `$getResults.psobject.properties) { `$getResultsHT[`$prop.Name] = `$prop.Value } } return `$getResultsHT "@ $GetScriptBlock = [ScriptBlock]::Create($GetScript) if ("$Get") { $splat.Get = $get } elseif ($GetScriptBlock) { $Splat.Get = $GetScriptBlock } } if ($boundParams.Test) { $splat.Test = $test } elseif ($TestCommand) { $TestScript = @" `$splat = @{} $(@(foreach ($kv in $paramNames.GetEnumerator()) { "if (`$psBoundParameters.ContainsKey('$($kv.Key)')) { `$splat['$($kv.Value)'] = $(if ($setcommand.Parameters[$kv.Value].ParameterType -like "*Automation.ScriptBlock*") {"[ScriptBlock]::Create(`$$($kv.Key))"} else {"`$$($kv.Key)" }) }"}) -join ([Environment]::NewLine)) $( foreach ($p in $TestParameterBlacklist) { " `$splat.Remove('$p') " }) $(foreach ($default in $TestDefaultParameter.GetEnumerator()) { " if (-not `$splat.ContainsKey('$($default.Key)')) { `$splat.$($default.Key) = $( if ($default.Value -is [string]) { "'$($default.Value)'" } elseif ($default.Value -is [Bool]) { "`$$($default.Value)" } else { "$($default.Value)" })" }) `$TestResults = `$testResult = $TestCommand @splat "@ if ($ProcessTestCommandResult) { $TestScript += $ProcessTestCommandResult } else { $TestScript += @" `$(`$testResults) -as [bool] "@ } $TestScriptBlock = [ScriptBlock]::Create($TestScript) if ($TestScriptBlock) { $Splat.Test = $TestScriptBlock } } Write-DSCResource @splat return } $schemaProperties = New-Object Collections.ArrayList $getParams = New-Object Collections.ArrayList $testParams = New-Object Collections.ArrayList $setParams = New-Object Collections.ArrayList # Go thru each property in the hashtable foreach ($kv in $Property.GetEnumerator()) { $attributeSection = New-Object Collections.ArrayList $propertyName = $kv.Key $PropertyDescription, $propertyType, $propertyRest = @($kv.Value) if (-not $PropertyType) { $PropertyType = [string] } if (-not $propertyRest) { $propertyRest = @{} } elseif ($propertyRest -as [Hashtable]) { foreach ($pr in ($propertyRest -as [Hashtable]).GetEnumerator()) { $matchingParam = $null if ($myCmd.Parameters.ContainsKey($pr.Key)) { $matchingParam = $myCmd.Parameters[$pr.Key] } else { foreach ($p in $myCmd.Parameters) { if ($p.Aliases -contains $pr.Key) { $matchingParam = $p break } } } if ($matchingParam) { $existingVar = $ExecutionContext.SessionState.PSVariable.Get($pr.Key) if ($existingVar -and $existingVar.Value -and $existingVar.Value.GetType().IsArray) { $existingVar += $pr.Value } elseif (-not $existingVar) { $ExecutionContext.SessionState.PSVariable.Set($pr.Key, $pr.Value) } } elseif ('ValidValue', 'ValidValues','Value', 'Values', 'ValueMap' -contains $pr.Key) { $PropertyValueMap.$propertyname = $pr.Value } } } if ($KeyProperty -contains $propertyName) { $null = $attributeSection.Add('Key') } if ($ReadOnlyProperty -contains $propertyName) { $null = $attributeSection.Add('Read') } elseif (-not ($KeyProperty -contains $propertyName)) { $null = $attributeSection.Add('Write') } $isMandatory = $KeyProperty -contains $propertyName -or $MandatoryProperty -contains $propertyName if ($PropertyDescription) { $null = $attributeSection.Add("Description(`"$($PropertyDescription.Replace('\','\\').Replace('"', '\"'))`")") } if ($PropertyValueMap.ContainsKey($propertyName)) { if ($PropertyValueMap[$propertyName] -is [Hashtable]) { $valueKeys = New-Object Collections.ArrayList $valueValues = New-Object Collections.ArrayList foreach ($kv in $PropertyValueMap[$propertyName].GetEnumerator()) { $null = $valueKeys.Add($kv.Key) $null = $valueValues.Add($kv.Value) } $null = $attributeSection.Add("ValueMap{`"$($valueKeys -join '","')`"}") $null = $attributeSection.Add("Values{`"$($valueValues -join '","')`"}") } else { $null = $attributeSection.Add("ValueMap{`"$($PropertyValueMap[$propertyName] -join '","')`"}") $null = $attributeSection.Add("Values{`"$($PropertyValueMap[$propertyName] -join '","')`"}") } } $propertyPowerShellType = if ($PropertyType -eq [Hashtable]) { 'Microsoft.Management.Infrastructure.CimInstance[]' } elseif ($propertyType -eq [ScriptBlock]) { 'string' } elseif ($propertyType -eq [ScriptBlock[]]) { 'string[]' } elseif ($propertyType -eq [TimeSpan]) { 'string' } elseif ($propertyType -eq [TimeSpan[]]) { 'string[]' } else { $PropertyType } $paramStr = @" [Parameter($(if ($isMandatory) { 'Mandatory=$true'}))] [$propertyPowerShellType] `$$PropertyName "@ if ($isMandatory) { $null = $getParams.Add($paramStr) } if ($ReadOnlyProperty -notcontains $PropertyName) { $null = $setParams.Add($paramStr) $null = $testParams.Add($paramStr) } $cimTypeString = if ($PropertyType -and $CimTypeMap[$PropertyType]) { if ($EmbeddedInstances.Contains($PropertyType)) { $null = $attributeSection.Add("EmbeddedInstance(`"$($EmbeddedInstances[$PropertyType])`")") 'String' } else { $CimTypeMap[$PropertyType] } } else { 'String' } $ArrayMarker = '' if ($acc.PropertyType.IsArray -or $PropertyType -eq [Hashtable]) { $ArrayMarker = '[]' } if ($cimTypeString.EndsWith('[]')) { $cimTypeString = $cimTypeString.TrimEnd('[]') $ArrayMarker = '[]' } $null = $schemaProperties.Add("`t$(if ($attributeSection.Count -ge 1 ) { "[$($attributeSection -join ', ')]" }) $CimTypeString $($PropertyName)$ArrayMarker;") } $DscGet = @" function Get-TargetResource { [CmdletBinding()] [OutputType([Hashtable])] param($($getParams -join ', ')) $Get } "@ $DscSet = @" function Set-TargetResource { [CmdletBinding()] param($($setParams -join ', ')) $Set } "@ $dscTest = @" function Test-TargetResource { [CmdletBinding()] [OutputType([bool])] param($($testParams -join ', ')) $test } "@ $psm1 = " $DscGet $DscSet $dscTest Export-ModuleMember -Function Get-TargetResource, Set-TargetResource, Test-TargetResource " if (-not $FriendlyName) { $FriendlyName = $ResourceName } $versionString = "$($version.Major).$($version.Minor).$(if ($version.Build -gt 0) { $version.Build} else {0 }).$(if ($version.Revision -gt 0) {$version.Revision} else {0 } )" $resourceMof = @" [ClassVersion("$VersionString"), FriendlyName("$FriendlyName")] class $ResourceName : OMI_BaseResource { $($schemaProperties -join ([Environment]::NewLine)) }; "@ if ($ModuleRoot) { $resourceDir = Join-Path $moduleRoot "DSCResources" $resourceDir = Join-Path $resourceDir $ResourceName if (-not (Test-Path $resourceDir)) { $ni = New-Item -ItemType 'Directory' -Path $resourceDir -Force if (-not $ni ) { return } } $psm1 | Set-Content -Path (Join-Path $resourceDir "$ResourceName.psm1") $resourceMof | Set-Content -Path (Join-Path $resourceDir "$ResourceName.schema.mof") } else { $output = New-Object PSObject | Add-Member NoteProperty "$ResourceName.psm1" "$psm1" -PassThru | Add-Member NoteProperty "$ResourceName.schema.mof" "$resourceMof" -PassThru $output.pstypenames.clear() $output.pstypenames.add('cFg.DSC.Resource') $output } } } |