internal/functions/Get-DscResourceParameterInfoByCimClass.ps1

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
Function Get-DscResourceParameterInfoByCimClass {
  <#
    .SYNOPSIS
      Retrieve the DSC Parameter information, if possible, by CIM Instance.
 
    .DESCRIPTION
      Given a DSC Resource Info object, load its CIM Class by invoking it once (ignoring errors) and then
      introspecting its CIM class information in the DSC namespace. This requires running with administrator
      privileges, unfortunately, as access to the CIM classes is privilege-gated.
 
      It will discover help documentation if it is surfaced in the CIM class (not all resources do so), will
      retrieve and map embedded CIM instances (which `Get-DscResourceParameterInfo` cannot do), but cannot
      retrieve default values as these are not mapped.
 
    .PARAMETER DscResource
      A Dsc Resource Info object, as returned by Get-DscResource.
 
    .EXAMPLE
      Get-DscResource -Name PSRepository | Get-DscResourceParameterInfoByCimClass
 
      Retrieve the Parameter information from the CIM class for the PSRepository Dsc Resource.
  #>

  [CmdletBinding()]
  param (
    [Parameter(ValueFromPipeline)]
    [Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo]$DscResource
  )

  Begin {}

  Process {
    # We can assume it will find the right version because this is only ever called after we've munged the PSModulePath
    $ModulePath = Get-Module -ListAvailable -Name $DscResource.ModuleName | Select-Object -ExpandProperty Path
    # Invoke once to load the CIM class information, ignore all errors
    Initialize-DscResourceCimClass -Name $DscResource.Name -ModuleName $ModulePath -ModuleVersion $DscResource.Version -ErrorAction Stop

    # Look for embedded instances, store them for type-definition interpolation.
    $DefinedEmbeddedInstances = @{}
    $EmbeddedInstanceTypes = Get-EmbeddedCimInstance -ClassName $DscResource.ResourceType -Recurse
    If ($EmbeddedInstanceTypes.count -gt 0) {
      # Parse Embedded Instances in reverse, which should figure out nested instances before those that contain them
      [array]::Reverse($EmbeddedInstanceTypes)
      ForEach ($InstanceType in $EmbeddedInstanceTypes) {
        # Handle credential objects separately as they are well-known constructs
        If ($InstanceType -eq 'MSFT_Credential') {
          $DefinedEmbeddedInstances.$InstanceType = 'Optional[Struct[{ user => String[1], password => Sensitive[String[1]] }]]'
        } Else {
          # Capture the metadata in order to parse the Puppet type definition and retrieve the cim instance types.
          $EmbeddedInstanceMetadata = @{}
          $EmbeddedInstanceMetadata.$InstanceType = @{
            cim_instance_type = "'$InstanceType'"
          }
          $CimClassProperties = Get-CimClassPropertiesList -ClassName $InstanceType
          ForEach($Property in $CimClassProperties) {
            If ($Property.ReferenceClassName -in $DefinedEmbeddedInstances.Keys) {
              # Handle nested instances, wrapping them in the Array datatype if necessary
              If ($Property.CimType -match 'Array') {
                $EmbeddedInstanceMetadata.$InstanceType.$($Property.Name) = "Array[$($DefinedEmbeddedInstances.($Property.ReferenceClassName))]"
              } Else {
                $EmbeddedInstanceMetadata.$InstanceType.$($Property.Name) = $DefinedEmbeddedInstances.($Property.ReferenceClassName)
              }
            } Else {
              # If it's not a CIM instance the standard type mapper can handle it.
              $EmbeddedInstanceMetadata.$InstanceType.$($Property.Name) = Get-PuppetDataType -DscResourceProperty @{
                Values       = $Property.Qualifiers['Values'].Value
                IsMandatory  = $Property.Flags -Match 'Required'
                # Replace the Array identifier with [] to match current expectations
                PropertyType = $Property.CimType -Replace '(\S+)Array$','$1[]'
              }
            }
          }
          # Nested CIM instances need to be reassembled into readable Structs; but we want to increase the indentation level by one
          # so that it's more visually distinct in the end file
          $StructComponents = $EmbeddedInstanceMetadata.$InstanceType.Keys |
            ForEach-Object -Process { "$_ => $($EmbeddedInstanceMetadata.$InstanceType.$_ -replace "`n", "`n ")" }
          # Assemble the current CIM instance as a struct, strip out any double quotes to prevent breaking parsing
          $DefinedEmbeddedInstances.$InstanceType = "Struct[{`n $($StructComponents -join ",`n " -replace '"')`n}]"
        }
      }
    }

    # Do some slight property handling to ignore properties we don't care about.
    # Minimally adapted from Ansible's implementation:
    # - https://github.com/ansible-collections/ansible.windows/blob/master/plugins/modules/win_dsc.ps1#L42-L62
    # Which itself borrows from core DSC:
    # - https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/DscSupport/CimDSCParser.cs#L1203
    $PropertiesToDiscard = @('ConfigurationName', 'DependsOn', 'ModuleName', 'ModuleVersion', 'ResourceID', 'SourceInfo')
    $DscResourceCimClassProperties = Get-CimClassPropertiesList -ClassName $DscResource.ResourceType |
      Where-Object {
        $_.Name -notin $PropertiesToDiscard -and
        -not $_.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::ReadOnly)
      }

    $DscResourceMetadata = @{}

    # Similarly to how the properties were resolved for embedded CIM instances, resolve them for each property
    ForEach($Property in $DscResourceCimClassProperties) {
      $DscResourceMetadata.$($Property.Name) = [ordered]@{
        Name = $Property.Name.ToLowerInvariant()
        # The one thing we *can't* retrieve here is the default values; they still apply, but they're
        # not exposed in the definition here for some reason. In the alternate implementation, we can
        # only retrieve default values by parsing the AST, so this is acceptable, if not ideal.
        DefaultValue = $null
        Help = $Property.Qualifiers['Description'].Value
        is_namevar        = ($Property.Flags -Match 'Key').ToString().ToLowerInvariant()
        mandatory_for_get = ($Property.Flags -Match '(Required|Key)').ToString().ToLowerInvariant()
        mandatory_for_set = ($Property.Flags -Match '(Required|Key)').ToString().ToLowerInvariant()
        mof_is_embedded   = 'false'
      }
      If ($Property.ReferenceClassName -in $DefinedEmbeddedInstances.Keys) {
        $DscResourceMetadata.$($Property.Name).mof_is_embedded = 'true'
        $MofType = $Property.ReferenceClassName
        # Munge the type name per the expectations/surface from Get-DscResource and existing provider.
        If ($MofType -eq 'MSFT_Credential') { $MofType = "PSCredential"}
        $DscResourceMetadata.$($Property.Name).mof_type = if ($Property.CimType -match 'Array') {
                                                            "$MofType[]"
                                                          } Else {
                                                            $MofType
                                                          }
        # Split the definition for the struct and toss away the cim_instance_type key as this is a top-level property
        # and that information is captured in the mof_type key already.
        $SplitDefinition = $DefinedEmbeddedInstances.($Property.ReferenceClassName) -split "`n" |
          Where-Object -FilterScript {$_ -notmatch "cim_instance_type => '$($Property.ReferenceClassName)'"}
        # Recombine the struct definition appropriately mapped as an array or singleton
        If ($Property.CimType -match 'Array') {
          $DscResourceMetadata.$($Property.Name).Type = """Array[$($SplitDefinition -Join "`n")]"""
        } Else {
          $DscResourceMetadata.$($Property.Name).Type = """$($SplitDefinition -Join "`n")"""
        }
      } Else {
        $DscResourceMetadata.$($Property.Name).mof_type = $Property.CimType -Replace '(\S+)Array$','$1[]'
        $DscResourceMetadata.$($Property.Name).Type     = Get-PuppetDataType -DscResourceProperty @{
          Values       = $Property.Qualifiers['Values'].Value
          IsMandatory  = $Property.Flags -Match '(Required|Key)'
          # Replace the Array identifier with [] to match current expectations
          PropertyType = $Property.CimType -Replace '(\S+)Array$','[$1[]]'
        }
      }
    }

    ForEach ($Property in $DscResourceMetadata.Keys) {
      # Each object has the Name, DefaultValue, Help, mandatory_for_get, mandatory_for_set, mof_type, & Type properties
      # This is the surface that Get-TypeParameterContent expects for processing a resource.
      [PSCustomObject]$DscResourceMetadata.$Property
    }
  }

  End {}
}