-
Notifications
You must be signed in to change notification settings - Fork 21
/
psDscAdapter.psm1
413 lines (366 loc) · 19.6 KB
/
psDscAdapter.psm1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# if the version of PowerShell is greater than 5, import the PSDesiredStateConfiguration module
# this is necessary because the module is not included in the PowerShell 7.0+ releases;
# In Windows PowerShell, we should always use version 1.1 that ships in Windows.
if ($PSVersionTable.PSVersion.Major -gt 5) {
$m = Get-Module PSDesiredStateConfiguration -ListAvailable | Sort-Object -Descending | Select-Object -First 1
$PSDesiredStateConfiguration = Import-Module $m -Force -PassThru
}
else {
$env:PSModulePath += ";$env:windir\System32\WindowsPowerShell\v1.0\Modules"
$PSDesiredStateConfiguration = Import-Module -Name 'PSDesiredStateConfiguration' -RequiredVersion '1.1' -Force -PassThru -ErrorAction stop -ErrorVariable $importModuleError
if (-not [string]::IsNullOrEmpty($importModuleError)) {
$trace = @{'Debug' = 'ERROR: Could not import PSDesiredStateConfiguration 1.1 in Windows PowerShell. ' + $importModuleError } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
}
}
<# public function Invoke-DscCacheRefresh
.SYNOPSIS
This function caches the results of the Get-DscResource call to optimize performance.
.DESCRIPTION
This function is designed to improve the performance of DSC operations by caching the results of the Get-DscResource call.
By storing the results, subsequent calls to Get-DscResource can retrieve the cached data instead of making a new call each time.
This can significantly speed up operations that need to repeatedly access DSC resources.
.EXAMPLE
Invoke-DscCacheRefresh -Module "PSDesiredStateConfiguration"
#>
function Invoke-DscCacheRefresh {
[CmdletBinding(HelpUri = '')]
param(
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Object[]]
$Module
)
# create a list object to store cache of Get-DscResource
[dscResourceCache[]]$dscResourceCache = [System.Collections.Generic.List[Object]]::new()
# improve by performance by having the option to only get details for named modules
# workaround for File and SignatureValidation resources that ship in Windows
if ($null -ne $module -and 'PSDesiredStateConfiguration' -ne $module) {
if ($module.gettype().name -eq 'string') {
$module = @($module)
}
$DscResources = [System.Collections.Generic.List[Object]]::new()
$Modules = [System.Collections.Generic.List[Object]]::new()
foreach ($m in $module) {
$DscResources += Get-DscResource -Module $m
$Modules += Get-Module -Name $m -ListAvailable
}
}
elseif ('PSDesiredStateConfiguration' -eq $module -and $PSVersionTable.PSVersion.Major -le 5 ) {
# the resources in Windows should only load in Windows PowerShell
# workaround: the binary modules don't have a module name, so we have to special case File and SignatureValidation resources that ship in Windows
$DscResources = Get-DscResource | Where-Object { $_.modulename -eq 'PSDesiredStateConfiguration' -or ( $_.modulename -eq $null -and $_.parentpath -like "$env:windir\System32\Configuration\*" ) }
}
else {
# if no module is specified, get all resources
$DscResources = Get-DscResource
$Modules = Get-Module -ListAvailable
}
$psdscVersion = Get-Module PSDesiredStateConfiguration | Sort-Object -descending | Select-Object -First 1 | ForEach-Object Version
foreach ($dscResource in $DscResources) {
# resources that shipped in Windows should only be used with Windows PowerShell
if ($dscResource.ParentPath -like "$env:windir\System32\*" -and $PSVersionTable.PSVersion.Major -gt 5) {
continue
}
# we can't run this check in PSDesiredStateConfiguration 1.1 because the property doesn't exist
if ( $psdscVersion -ge '2.0.7' ) {
# only support known dscResourceType
if ([dscResourceType].GetEnumNames() -notcontains $dscResource.ImplementationDetail) {
$trace = @{'Debug' = 'WARNING: implementation detail not found: ' + $dscResource.ImplementationDetail } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
continue
}
}
# workaround: if the resource does not have a module name, get it from parent path
# workaround: modulename is not settable, so clone the object without being read-only
# workaround: we have to special case File and SignatureValidation resources that ship in Windows
$binaryBuiltInModulePaths = @(
"$env:windir\system32\Configuration\Schema\MSFT_FileDirectoryConfiguration"
"$env:windir\system32\Configuration\BaseRegistration"
)
$DscResourceInfo = [DscResourceInfo]::new()
$dscResource.PSObject.Properties | ForEach-Object -Process {
if ($null -ne $_.Value) {
$DscResourceInfo.$($_.Name) = $_.Value
}
else {
$DscResourceInfo.$($_.Name) = ''
}
}
if ($dscResource.ModuleName) {
$moduleName = $dscResource.ModuleName
}
elseif ($binaryBuiltInModulePaths -contains $dscResource.ParentPath) {
$moduleName = 'PSDesiredStateConfiguration'
$DscResourceInfo.Module = 'PSDesiredStateConfiguration'
$DscResourceInfo.ModuleName = 'PSDesiredStateConfiguration'
$DscResourceInfo.CompanyName = 'Microsoft Corporation'
$DscResourceInfo.Version = '1.0.0'
if ($PSVersionTable.PSVersion.Major -le 5 -and $DscResourceInfo.ImplementedAs -eq 'Binary') {
$DscResourceInfo.ImplementationDetail = 'Binary'
}
}
elseif ($binaryBuiltInModulePaths -notcontains $dscResource.ParentPath -and $null -ne $dscResource.ParentPath) {
# workaround: populate module name from parent path that is three levels up
$moduleName = Split-Path $dscResource.ParentPath | Split-Path | Split-Path -Leaf
$DscResourceInfo.Module = $moduleName
$DscResourceInfo.ModuleName = $moduleName
# workaround: populate module version from psmoduleinfo if available
if ($moduleInfo = $Modules | Where-Object { $_.Name -eq $moduleName }) {
$moduleInfo = $moduleInfo | Sort-Object -Property Version -Descending | Select-Object -First 1
$DscResourceInfo.Version = $moduleInfo.Version.ToString()
}
}
$dscResourceCache += [dscResourceCache]@{
Type = "$moduleName/$($dscResource.Name)"
DscResourceInfo = $DscResourceInfo
}
}
return $dscResourceCache
}
# Convert the INPUT to a dscResourceObject object so configuration and resource are standardized as much as possible
function Get-DscResourceObject {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
$jsonInput
)
# normalize the INPUT object to an array of dscResourceObject objects
$inputObj = $jsonInput | ConvertFrom-Json
$desiredState = [System.Collections.Generic.List[Object]]::new()
# catch potential for improperly formatted configuration input
if ($inputObj.resources -and -not $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') {
$trace = @{'Debug' = 'WARNING: 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"}}' } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
}
# match adapter to version of powershell
if ($PSVersionTable.PSVersion.Major -le 5) {
$adapterName = 'Microsoft.Windows/WindowsPowerShell'
}
else {
$adapterName = 'Microsoft.DSC/PowerShell'
}
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.type
$inputObj.psobject.properties.Remove('type')
$desiredState += [dscResourceObject]@{
name = $adapterName
type = $type
properties = $inputObj
}
}
return $desiredState
}
# Get the actual state using DSC Get method from any type of DSC resource
function Invoke-DscOperation {
param(
[Parameter(Mandatory)]
[ValidateSet('Get', 'Set', 'Test', 'Export')]
[string]$Operation,
[Parameter(Mandatory, ValueFromPipeline = $true)]
[dscResourceObject]$DesiredState,
[Parameter(Mandatory)]
[dscResourceCache[]]$dscResourceCache
)
$osVersion = [System.Environment]::OSVersion.VersionString
$trace = @{'Debug' = 'OS version: ' + $osVersion } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
$psVersion = $PSVersionTable.PSVersion.ToString()
$trace = @{'Debug' = 'PowerShell version: ' + $psVersion } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
$moduleVersion = Get-Module PSDesiredStateConfiguration | ForEach-Object Version
$trace = @{'Debug' = 'PSDesiredStateConfiguration module version: ' + $moduleVersion } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
# get details from cache about the DSC resource, if it exists
$cachedDscResourceInfo = $dscResourceCache | Where-Object Type -EQ $DesiredState.type | ForEach-Object DscResourceInfo
# if the resource is found in the cache, get the actual state
if ($cachedDscResourceInfo) {
# formated OUTPUT of each resource
$addToActualState = [dscResourceObject]@{}
# set top level properties of the OUTPUT object from INPUT object
$DesiredState.psobject.properties | ForEach-Object -Process {
if ($_.TypeNameOfValue -EQ 'System.String') { $addToActualState.$($_.Name) = $DesiredState.($_.Name) }
}
$trace = @{'Debug' = 'DSC resource implementation: ' + [dscResourceType]$cachedDscResourceInfo.ImplementationDetail } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
# workaround: script based resources do not validate Get parameter consistency, so we need to remove any parameters the author chose not to include in Get-TargetResource
switch ([dscResourceType]$cachedDscResourceInfo.ImplementationDetail) {
'ScriptBased' {
# For Linux/MacOS, only class based resources are supported and are called directly.
if ($IsLinux) {
$trace = @{'Debug' = 'ERROR: Script based resources are only supported on Windows.' } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
# imports the .psm1 file for the DSC resource as a PowerShell module and stores the list of parameters
Import-Module -Scope Local -Name $cachedDscResourceInfo.path -Force -ErrorAction stop
$validParams = (Get-Command -Module $cachedDscResourceInfo.ResourceType -Name 'Get-TargetResource').Parameters.Keys
if ($Operation -eq 'Get') {
# prune any properties that are not valid parameters of Get-TargetResource
$DesiredState.properties.psobject.properties | ForEach-Object -Process {
if ($validParams -notcontains $_.Name) {
$DesiredState.properties.psobject.properties.Remove($_.Name)
}
}
}
# morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource
$DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { $property[$_.Name] = $_.Value }
# using the cmdlet the appropriate dsc module, and handle errors
try {
$invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property
if ($invokeResult.GetType().Name -eq 'Hashtable') {
$invokeResult.keys | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $invokeResult.$_ }
}
else {
# the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties
$invokeResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $invokeResult.$_ }
}
# set the properties of the OUTPUT object from the result of Get-TargetResource
$addToActualState.properties = $getDscResult
}
catch {
$trace = @{'Debug' = 'ERROR: ' + $_.Exception.Message } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
}
'ClassBased' {
try {
# load powershell class from external module
$resource = GetTypeInstanceFromModule -modulename $cachedDscResourceInfo.ModuleName -classname $cachedDscResourceInfo.Name
$dscResourceInstance = $resource::New()
if ($DesiredState.properties) {
# set each property of $dscResourceInstance to the value of the property in the $desiredState INPUT object
$DesiredState.properties.psobject.properties | ForEach-Object -Process {
$dscResourceInstance.$($_.Name) = $_.Value
}
}
switch ($Operation) {
'Get' {
$Result = $dscResourceInstance.Get()
$addToActualState.properties = $Result
}
'Set' {
$dscResourceInstance.Set()
}
'Test' {
$Result = $dscResourceInstance.Test()
$addToActualState.properties = [psobject]@{'InDesiredState'=$Result}
}
'Export' {
$t = $dscResourceInstance.GetType()
$method = $t.GetMethod('Export')
$resultArray = $method.Invoke($null,$null)
$addToActualState = $resultArray
}
}
}
catch {
$trace = @{'Debug' = 'ERROR: ' + $_.Exception.Message } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
}
'Binary' {
if ($PSVersionTable.PSVersion.Major -gt 5) {
$trace = @{'Debug' = 'To use a binary resource such as File, Log, or SignatureValidation, use the Microsoft.Windows/WindowsPowerShell adapter.' } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
if (-not (($cachedDscResourceInfo.ImplementedAs -eq 'Binary') -and ('File', 'Log', 'SignatureValidation' -contains $cachedDscResourceInfo.Name))) {
$trace = @{'Debug' = 'Only File, Log, and SignatureValidation are supported as Binary resources.' } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
# morph the INPUT object into a hashtable named "property" for the cmdlet Invoke-DscResource
$DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { $property[$_.Name] = $_.Value }
# using the cmdlet from PSDesiredStateConfiguration module in Windows
try {
$getResult = $PSDesiredStateConfiguration.invoke({ param($Name, $Property) Invoke-DscResource -Name $Name -Method Get -ModuleName @{ModuleName = 'PSDesiredStateConfiguration'; ModuleVersion = '1.1' } -Property $Property -ErrorAction Stop }, $cachedDscResourceInfo.Name, $property )
if ($getResult.GetType().Name -eq 'Hashtable') {
$getResult.keys | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $getResult.$_ }
}
else {
# the object returned by WMI is a CIM instance with a lot of additional data. only return DSC properties
$getResult.psobject.Properties.name | Where-Object { 'CimClass', 'CimInstanceProperties', 'CimSystemProperties' -notcontains $_ } | ForEach-Object -Begin { $getDscResult = @{} } -Process { $getDscResult[$_] = $getResult.$_ }
}
# set the properties of the OUTPUT object from the result of Get-TargetResource
$addToActualState.properties = $getDscResult
}
catch {
$trace = @{'Debug' = 'ERROR: ' + $_.Exception.Message } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
}
Default {
$trace = @{'Debug' = 'Can not find implementation of type: ' + $cachedDscResourceInfo.ImplementationDetail } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
}
return $addToActualState
}
else {
$dsJSON = $DesiredState | ConvertTo-Json -Depth 10
$errmsg = 'Can not find type "' + $DesiredState.type + '" for resource "' + $dsJSON + '". Please ensure that Get-DscResource returns this resource type.'
$trace = @{'Debug' = 'ERROR: ' + $errmsg } | ConvertTo-Json -Compress
$host.ui.WriteErrorLine($trace)
exit 1
}
}
# GetTypeInstanceFromModule function to get the type instance from the module
function GetTypeInstanceFromModule {
param(
[Parameter(Mandatory = $true)]
[string] $modulename,
[Parameter(Mandatory = $true)]
[string] $classname
)
$instance = & (Import-Module $modulename -PassThru) ([scriptblock]::Create("'$classname' -as 'type'"))
return $instance
}
# cached resource
class dscResourceCache {
[string] $Type
[psobject] $DscResourceInfo
}
# format expected for configuration and resource output
class dscResourceObject {
[string] $name
[string] $type
[psobject] $properties
}
# dsc resource types
enum dscResourceType {
ScriptBased
ClassBased
Binary
Composite
}
# dsc resource type (settable clone)
class DscResourceInfo {
[dscResourceType] $ImplementationDetail
[string] $ResourceType
[string] $Name
[string] $FriendlyName
[string] $Module
[string] $ModuleName
[string] $Version
[string] $Path
[string] $ParentPath
[string] $ImplementedAs
[string] $CompanyName
[psobject[]] $Properties
}