Skip to content

Instantly share code, notes, and snippets.

@CJHarmath
Created February 28, 2017 22:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CJHarmath/f2918d4f0dc7e14bdff1fa579b49ebbb to your computer and use it in GitHub Desktop.
Save CJHarmath/f2918d4f0dc7e14bdff1fa579b49ebbb to your computer and use it in GitHub Desktop.
try catch workaround for PSDscResources/issues/43
###########################################################
#
# 'PSDesiredStateConfiguration' logic module
#
###########################################################
data LocalizedData
{
# culture="en-US"
ConvertFrom-StringData -StringData @'
CheckSumFileExists = File '{0}' already exists. Please specify -Force parameter to overwrite existing checksum files.
CreateChecksumFile = Create checksum file '{0}'
OverwriteChecksumFile = Overwrite checksum file '{0}'
OutpathConflict = (ERROR) Cannot create directory '{0}'. A file exists with the same name.
InvalidConfigPath = (ERROR) Invalid configuration path '{0}' specified.
InvalidOutpath = (ERROR) Invalid OutPath '{0}' specified.
InvalidConfigurationName = Invalid Configuration Name '{0}' is specified. Standard names may only contain letters (a-z, A-Z), numbers (0-9), period (.), hyphen (-) and underscore (_). The name may not be null or empty, and should start with a letter.
NoValidConfigFileFound = No valid config files (mof,zip) were found.
InputFileNotExist=File {0} doesn't exist.
FileReadError=Error Reading file {0}.
MatchingFileNotFound=No matching file found.
CertificateFileReadError=Error Reading certificate file {0}.
CertificateStoreReadError=Error Reading certificate store for {0}.
CannotCreateOutputPath=Invalid Configuration name and output path combination :{0}. Please make sure output parameter is a valid path segment.
ConflictingDuplicateResource=A conflict was detected between resources '{0}' and '{1}' in node '{2}'. Resources have identical key properties but there are differences in the following non-key properties: '{3}'. Values '{4}' don't match values '{5}'. Please update these property values so that they are identical in both cases.
ConfiguratonDataNeedAllNodes=ConfigurationData parameter need to have property AllNodes.
ConfiguratonDataAllNodesNeedHashtable=ConfigurationData parameter property AllNodes needs to be a collection.
AllNodeNeedToBeHashtable=all elements of AllNodes need to be hashtable and has a property 'NodeName'.
DuplicatedNodeInConfigurationData=There are duplicated NodeNames '{0}' in the configurationData passed in.
EncryptedToPlaintextNotAllowed=Converting and storing encrypted passwords as plain text is not recommended. For more information on securing credentials in MOF file, please refer to MSDN blog: http://go.microsoft.com/fwlink/?LinkId=393729
DomainCredentialNotAllowed=It is not recommended to use domain credential for node '{0}'. In order to suppress the warning, you can add a property named 'PSDscAllowDomainUser' with a value of $true to your DSC configuration data for node '{0}'.
NestedNodeNotAllowed=Defining node '{0}' inside the current node '{1}' is not allowed since node definitions cannot be nested. Please move the definition for node '{0}' to the top level of the configuration '{2}'.
FailToProcessNode=An exception was raised while processing Node '{0}': {1}
LocalHostNodeNotAllowed=Defining a 'localhost' node in the configuration '{0}' is not allowed since the configuration already contains one or more resource definitions that are not associated with any nodes.
InvalidMOFDefinition=Invalid MOF definition for node '{0}': {1}
RequiredResourceNotFound=Resource '{0}' required by '{1}' does not exist. Please ensure that the required resource exists and the name is properly formed.
ReferencedManagerNotFound=Download Manager '{0}' referenced by '{1}' does not exist. Please ensure that the referenced download manager exists and the name is properly formed.
ReferencedResourceSourceNotFound=Resource Repository '{0}' referenced by '{1}' does not exist. Please ensure that the referenced resource repository exists and the name is properly formed.
DependsOnLinkTooDeep=DependsOn link exceeded max depth limitation '{0}'.
DependsOnLoopDetected=Circular DependsOn exists '{0}'. Please make sure there are no circular reference.
FailToProcessConfiguration=Compilation errors occurred while processing configuration '{0}'. Please review the errors reported in error stream and modify your configuration code appropriately.
FailToProcessProperty={0} error processing property '{1}' OF TYPE '{2}': {3}
NodeNameIsRequired=Node processing is skipped since the node name is empty.
ConvertValueToPropertyFailed=Cannot convert '{0}' to type '{1}' for property '{2}' in resource '{3}'.
ResourceNotFound=The term '{0}' is not recognized as the name of a {1}.
GetDscResourceInputName=The Get-DscResource input '{0}' parameter value is '{1}'.
ResourceNotMatched=Skipping resource '{0}' as it does not match the requested name.
InitializingClassCache=Initializing class cache
LoadingDefaultCimKeywords=Loading default CIM keywords
GettingModuleList=Getting module list
CreatingResourceList=Creating resource list
CreatingResource=Creating resource '{0}'.
SchemaFileForResource=Schema file name for resource {0}
UnsupportedReservedKeyword=The '{0}' keyword is not supported in this version of the language.
UnsupportedReservedProperty=The '{0}' property is not supported in this version of the language.
MetaConfigurationHasMoreThanOneLocalConfigurationManager=The meta configuration for node '{0}' contain more than one definitions for LocalConfigurationManager which is not allowed.
MetaConfigurationSettingsMissing=The settings definition for node '{0}' is missing. A default empty settings definition is added for the node.
ConflictInExclusiveResources=The partial configuration '{0}' and '{1}' have coflicting exclusive resource declarations.
ReferencedModuleNotExist=The referenced module '{0}' does not exist on the machine. Please use Get-DscResource to find out what exists on the machine.
ReferencedResourceNotExist=The referenced resource '{0}' does not exist on the machine. Please use Get-DscResource to find out what exists on the machine.
ReferencedModuleResourceNotExist=The referenced module\resource '{0}' does not exist on the machine. Please use Get-DscResource to find out what exists on the machine.
DuplicatedResourceInModules=The referenced resource '{0}' exists in module {1} and module {2} on the machine. Please make sure it exists in only one module.
CannotConvertStringToBool=Cannot convert value "System.String" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.
NoModulesPresent=There are no modules present in the system with the given module specification.
ImportDscResourceWarningForInbuiltResource=The configuration '{0}' is loading one or more built-in resources without explicitly importing associated modules. Add Import-DscResource -ModuleName 'PSDesiredStateConfiguration' to your configuration to avoid this message.
PasswordTooLong=An error occurred during encryption of a password in node '{0}'. Most likely the password entered is too long to be encrypted using the selected certificate. Please either use a shorter password or select a certificate with a larger key.
HashtableElementTypeNotAllowed=Value of type '{0}' is not allowed in hashtable. Supported types : [String], [Char], [Int64], [UInt64], [Double], [Bool] ,[DateTime] and [ScriptBlock].
PullModeWithoutDownloadManager=Meta configuration is set to pull mode which requires a DownloadManager to be specified.
PullModeWithoutConfigurationRepository=Meta configuration is set to pull mode which requires a ConfigurationRepository to be specified.
DownloadManagerWithoutPullMode=A DownloadManager is specified without setting the refresh mode into PULL.
ConfigurationRepositoryWithoutPullMode=A ConfigurationRepository is specified without setting the refresh mode into PULL.
ReferencedPolicyNotDefined=The referenced SignatureValidationPolicy '{0}' is not defined. Please define a SignatureValidation block with the same name.
IncorrectSignatureValidationPolicyFormat =The value provided for SignatureValidationPolicy has incorrect format. Please provide its value in the form of '[SignatureValidation]<Name>'.
'@
}
Set-StrictMode -Off
# In case localized resource is not available we revert back to English as defined in LocalizedData section so ignore the error instead of showing it to user.
Import-LocalizedData -BindingVariable LocalizedData -FileName PSDesiredStateConfiguration.Resource.psd1 -ErrorAction SilentlyContinue
$script:V1MetaConfigPropertyList = @('ConfigurationModeFrequencyMins', 'RebootNodeIfNeeded', 'ConfigurationMode', 'ActionAfterReboot', 'RefreshMode', 'CertificateID', 'ConfigurationID', 'DownloadManagerName', 'DownloadManagerCustomData', 'RefreshFrequencyMins', 'AllowModuleOverwrite', 'DebugMode', 'Credential')
$script:DirectAccessMetaConfigPropertyList = @('AllowModuleOverWrite', 'CertificateID', 'ConfigurationDownloadManagers', 'ResourceModuleManagers', 'DebugMode', 'RebootNodeIfNeeded', 'RefreshMode', 'ConfigurationAgent')
#################################################################
# Code to determin what version related info is needed
# and fixup the configuration document is it is already generated
#################################################################
function Generate-VersionInfo
{
param(
[Parameter(Mandatory)]
$KeywordData,
[Parameter(Mandatory)]
[Hashtable]
$Value
)
$SystemProperties = @('ResourceID', 'SourceInfo', 'ModuleName', 'ModuleVersion')
$HasAdditionalProperty = $false
foreach ($key in $KeywordData.Keys)
{
if (($Value.Contains($key)) -and ($script:V1MetaConfigPropertyList -notcontains $key) -and ($SystemProperties -notcontains $key))
{
$HasAdditionalProperty = $true
break
}
}
if($HasAdditionalProperty)
{
Set-PSMetaConfigVersionInfoV2
}
else
{
$script:PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'] = ($script:PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'], "1.0.0" | Measure-Object -Maximum).Maximum
}
$script:PSMetaConfigDocumentInstVersionInfo['CompatibleVersionAdditionalProperties'] = @('MSFT_DSCMetaConfiguration:StatusRetentionTimeInDays')
$script:PSMetaConfigurationProcessed = $true
}
# indicate whether meta configuration is processed before document instance
function Set-PSMetaConfigDocInsProcessedBeforeMeta
{
$Script:PSMetaConfigDocInsProcessedBeforeMeta = $true
}
function Get-PSMetaConfigurationProcessed
{
return $script:PSMetaConfigurationProcessed
}
function Get-PSMetaConfigDocumentInstVersionInfo
{
return $script:PSMetaConfigDocumentInstVersionInfo
}
function Set-PSMetaConfigVersionInfoV2
{
$script:PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'] = '2.0.0'
if($Script:PSMetaConfigDocInsProcessedBeforeMeta) #fixup configuration document instance version info
{
[string]$data = Get-MofInstanceText '$OMI_ConfigurationDocument1ref'
$Script:NoNameNodeInstanceAliases['$OMI_ConfigurationDocument1ref'] = $data -replace 'MinimumCompatibleVersion = "1.0.0"', 'MinimumCompatibleVersion = "2.0.0"'
Set-PSDefaultConfigurationDocument $Script:NoNameNodeInstanceAliases['$OMI_ConfigurationDocument1ref']
}
}
function Get-CompatibleVersionAddtionaPropertiesStr
{
'{'
if($script:PSMetaConfigDocumentInstVersionInfo['CompatibleVersionAdditionalProperties'])
{
$len = @($script:PSMetaConfigDocumentInstVersionInfo['CompatibleVersionAdditionalProperties']).Length
foreach ($e in @($script:PSMetaConfigDocumentInstVersionInfo['CompatibleVersionAdditionalProperties']))
{
"`"$e`"" + $(if (--$len -gt 0)
{
', '
}
else
{
''
}
)
}
}
'}'
}
###########################################################
# The MOF generation code
###########################################################
#
# This scriptblock takes a type name and a list of properties and produces
# the MOF source text to define an instance of that type
#
function ConvertTo-MOFInstance
{
param (
[Parameter(Mandatory)]
[string]
$Type,
[Parameter(Mandatory)]
[AllowNull()]
[hashtable]
$Properties
)
# remove ModuleVersion, this will be handled during final validation.
if($properties.ContainsKey('ModuleName') -and $properties['ModuleName'] -ieq 'PsDesiredStateConfiguration')
{
$script:PsDscModuleVersion = $properties['ModuleVersion']
$properties.Remove('ModuleVersion')
}
if($properties.ContainsKey('PsDscRunAsCredential'))
{
$script:PsDscCompatibleVersion = "2.0.0"
}
# Look up the property definitions for this keyword.
$PropertyTypes = [System.Management.Automation.Language.DynamicKeyword]::GetKeyword($Type).Properties
# and the CIM type name to use since the keyword might be an alias.
$ResourceName = [System.Management.Automation.Language.DynamicKeyword]::GetKeyword($Type).ResourceName
if($script:IsMetaConfig -and ($ResourceName -eq 'MSFT_DSCMetaConfigurationV2'))
{
Generate-VersionInfo $PropertyTypes $Properties
}
#
# Function to convert .NET datetime object to MOF datetime string format
# We're not using [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime()
# because it has known bugs which are not going to be fixed.
#
function ConvertTo-MofDateTimeString ([datetime] $d)
{
$utcOffset = ($d -$d.ToUniversalTime()).TotalMinutes
$utcOffsetString = if ($utcOffset -ge 0)
{
'+'
}
else
{
'-'
}
$utcOffsetString += ([System.Math]::Abs(($utcOffset)).ToString().PadLeft(3,'0'))
'{0}{1}' -f
$d.ToString('yyyyMMddHHmmss.ffffff'),
$utcOffsetString
}
#
# Utility routine to find if username specified is
# a domain user.
#
function IsDomainUser()
{
param(
[Parameter(Mandatory)]
[string]
$UserName
)
# if username contains '\' example: domain\username or '@' example: username@mydomain.com
# it may not be a local user.
if( -not( $UserName.Contains('\') -or $UserName.Contains('@')))
{
# it is a local user
return $false
}
elseif( $UserName.Contains('@'))
{
return $true
}
else
{
# In case of '\', domain name can be local machine.
$result = $UserName.Split('\')
if( $result.Count -ge 2)
{
$domain = $result[0]
$localMachineNames = @("localhost","127.0.0.1","::1",$env:COMPUTERNAME)
$isDomainUser = $true
if( $localMachineNames -icontains $domain)
{
$isDomainUser = $false
}
return $isDomainUser
}
}
$true
}
#
# Utility routine to render a property
# as a string in MOF syntax.
#
function stringify ($Value, $asArray = $false, $targetType = [string])
{
$result = if ($Value -is [array] -or $asArray)
{
'{'
$len = @($Value).Length
foreach ($e in $Value)
{
' ' + (stringify $e -targetType $targetType) +
$(if (--$len -gt 0)
{
','
}
else
{
''
}
)
}
'}'
}
elseif ($Value -is [PSCredential] )
{
# If the input object is a PSCredential, turn it into an MSFT_Credential with an encrypted password.
$clearText = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::GetStringFromSecureString($Value.Password)
$newValue = @{
UserName = $Value.UserName
Password = $clearText
}
# Recurse to build the object.
ConvertTo-MOFInstance MSFT_Credential $newValue
}
elseif ($Value -is [System.Collections.Hashtable])
{
# Collect the individual strings
$elementsAsStrings = foreach ($p in $Value.GetEnumerator())
{
if($p.Value -isnot [string] -and
$p.Value -isnot [char] -and
$p.Value -isnot [int64] -and
$p.Value -isnot [uint64] -and
$p.Value -isnot [double] -and
$p.Value -isnot [bool] -and
$p.Value -isnot [datetime] -and
$p.Value -isnot [ScriptBlock])
{
$errorMessage = $LocalizedData.HashtableElementTypeNotAllowed -f $($p.Value.GetType())
ThrowError -ExceptionName 'System.ArgumentException' -ExceptionMessage $errorMessage -ExceptionObject $Value -ErrorId 'NestedHashtableNotAllowed' -ErrorCategory InvalidArgument
}
ConvertTo-MOFInstance MSFT_KeyValuePair @{
Key = $p.Key
Value = $p.Value
}
}
# Produce a single formatted string.
' ' + ($elementsAsStrings -join ",`n ") + "`n"
}
elseif ($Value -is [ScriptBlock] )
{
# Find all $using: variables used in the script and replace them with normal variables
$scriptText = "$Value"
# Need to create a new scriptblock so the extent offsets are correct
$scriptAst = [scriptblock]::Create($scriptText).Ast
# get the $using: variable asts into an array
$variables = $scriptAst.FindAll({
param ($ast)
$ast.GetType().FullName -match 'VariableExpressionAst' -and
$ast.Extent.Text -match '^\$using:'
}
, $true).ToArray()
# do the substitutions in reverse order
[Array]::Reverse($variables)
$variables | ForEach-Object -Process {
$start = $_.Extent.StartOffset
$length = $_.Extent.EndOffset - $start
$newName = '$' + $_.VariablePath.UserPath
$scriptText = $scriptText.Remove($start, $length).Insert($start, $newName)
}
$completeScript = ''
# generate assignement statements to set the variable values on the other side
# using serialized values passed from the local environment
$varNames = @($variables).VariablePath.UserPath | Sort-Object -Unique
foreach ($v in $varNames)
{
# If the ScriptBlock was defined in a module, then use the module for lookups
if ($Value.Module)
{
$var = $Value.Module.SessionState.PSVariable.Get($v)
}
else
{
# Otherwise look up in the callers context
$var = $ExecutionContext.SessionState.Module.GetVariableFromCallersModule($v)
}
if ($var)
{
$varValue = $var.Value
# Skip null values but preserve empty arrays and strings for type propigation
if ($varValue -ne $null)
{
# Pass strings quoted; amn explicit type check is needed because -is recognizes too many things as strings
if ($varValue -is [string])
{
$completeScript += "`$$v ='" + ($varValue -replace "'", "''") + "'`n"
}
else
{
# Serialize everything else
$serializedValue = [System.Management.Automation.PSSerializer]::Serialize($varValue) -replace "'", "''"
$completeScript += "`$$v = [System.Management.Automation.PSSerializer]::Deserialize('$serializedValue')`n"
}
}
}
}
# Merge in the actual scriptblock body
$completeScript += $scriptText
# Quote the string so it's suitable to embed in the MOF file...
'"' + ($completeScript -replace '\\', '\\' -replace "[`r]*`n", '\n' -replace '"', '\"') + '"'
}
elseif ($targetType -eq [datetime])
{
# If the target is a datetime, convert the argument to a datetime and then render that
# as a DMTF datetime...
'"' + (ConvertTo-MofDateTimeString $Value) + '"'
}
elseif ($targetType -eq [double])
{
# MOF syntax requires reals to always have a decimal point so add
# so add one if the string representation does contain one.
[string] $dblAsString = [double] $Value
if ( -not $dblAsString.Contains('.') )
{
$dblAsString += '.0'
}
$dblAsString
}
elseif ($targetType -eq [char])
{
# A char16 is encode as a single quoted character
"'$Value'"
}
elseif ($targetType -eq [int64])
{
[int64] $Value
}
elseif ($targetType -eq [uint64])
{
[uint64] $Value
}
elseif ($targetType -eq [bool])
{
if($Value -is [string])
{
$errorMessage = $LocalizedData.CannotConvertStringToBool
ThrowError -ExceptionName 'System.ArgumentException' -ExceptionMessage $errorMessage -ExceptionObject $Value -ErrorId 'CannotConvertStringToBool' -ErrorCategory InvalidArgument
}
else
{
[bool]$Value
}
}
elseif ($targetType -ne [string])
{
# Cast the value to the target type...
$Value -as $targetType
}
elseif ($Value -is [string] -and -not $InstanceAliases[$Value] )
{
'"' + ($Value -replace '\\', '\\' -replace "`r?`n", '\n' -replace '"', '\"') + '"'
}
elseif ($Value -eq $null)
{
'NULL'
}
elseif (($targetType -eq [string]) -and ($Value -isnot [string]))
{
# Cast value to string if it is not already a string, this is for covering cases like when a user assign an integer while the
# CIM property type is string
'"' + ($Value -replace '\\', '\\' -replace "`r?`n", '\n' -replace '"', '\"') + '"'
}
else
{
$Value
}
$result -join "`n"
}
Write-Debug -Message " BEGIN MOF GENERATION FOR $Type"
# Generate the MOF instance alias to use for the current node
if ( (Get-PSCurrentConfigurationNode) )
{
if($Script:NodeTypeRefCount[ (Get-PSCurrentConfigurationNode) ] -eq $null)
{
$Script:NodeTypeRefCount[ (Get-PSCurrentConfigurationNode) ] =
New-Object -TypeName 'System.Collections.Generic.Dictionary[string, System.Collections.Generic.Dictionary[string,int]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
$MofAliasString = '$' + $ResourceName + ++$Script:NodeTypeRefCount[ (Get-PSCurrentConfigurationNode) ][$ResourceName] + 'ref'
$InstanceAliases = $Script:NodeInstanceAliases[ (Get-PSCurrentConfigurationNode) ]
}
else # Generate the MOF instance alias to use for the default (unnamed) node.
{
$MofAliasString = '$' + $ResourceName + ++$Script:NoNameNodeTypeRefCount[$ResourceName] + 'ref'
$InstanceAliases = $Script:NoNameNodeInstanceAliases
}
# Start generating the MOF source text for this instance
$result = "instance of $ResourceName as $MofAliasString`n{`n"
#special case psdesiredstateConfiguration module. Insert '0.0' as module if user hasn't explicilty asked for the version
if( $Properties.ContainsKey("ModuleName") -and ($Properties["ModuleName"] -ieq 'PsDesiredStateConfiguration'))
{
if(-not $Script:ExplicitlyImportedModules.ContainsKey('PsDesiredStateConfiguration'))
{
$script:ShowImportDscResourceWarning = $true
}
}
# generate the property definitions
$oldOFS = $OFS
$OFS = ' '
try
{
if ($Properties -and $Properties.Count)
{
$result += foreach ($p in $Properties.GetEnumerator())
{
Write-Debug -Message " Generating property data for '$($p.Name)' = '$($p.Value)'"
if ($p.Name -eq 'RefreshMode' -and $p.Value -eq 'PULL')
{
$script:MetaConfigSetToPull = $true
}
if ($p.Name -eq 'DownloadManagerName')
{
$script:SpecifiedV1DownloadManagerName = $true
}
$targetTypeName = $PropertyTypes[$p.Name].TypeConstraint
# see if the target type is an array
$asArray = $p.Name -eq 'DependsOn' -or $targetTypeName -match 'Array' -or ((-not [string]::IsNullOrEmpty($targetTypeName)) -and $targetTypeName.EndsWith('[]'))
# Convert the CIM typename to the appropriate .NET type to use
# to convert the input object into an appropriately encoded string
# using the PowerShell type conversion semantics.
switch -regex ($targetTypeName)
{
# unsigned integer types
'^sint[0-9]{1,2}'
{
$targetType = [int64]
break
}
# Single 16 bit character (note - this type is deprecated and removed in MOFv3
'^char16'
{
$targetType = [char]
break
}
# signed integer types
'^uint[0-9]{0,2}'
{
$targetType = [uint64]
break
}
# reals
'^real32|^real64'
{
$targetType = [double]
break
}
# boolean
'^boolean'
{
$targetType = [bool]
}
# datetime
'datetime'
{
$targetType = [datetime]
}
# everything else render directly as a string...
default
{
$targetType = [string]
}
}
#
# If the scalar target types is a credential, then we need to encrypt the Password property value
# before generating the MOF text
#
if(($Type -match 'MSFT_Credential') -and $p.Name -match 'Password')
{
# For MSFT_Credential we'll have a password that may need to be encrypted depending
# on the availability of a key. This may need to change to the base class of MSFT_WindowCredential
# if we're using the class to refer to non-Windows machines where a Domain may be irrelevant.
$p.Name + ' = ' + (stringify -value (Get-EncryptedPassword $p.Value) -asArray $asArray -targetType $targetType ) + ";`n"
}
else
{
#embeded instances cannot be null
if($p.Value -eq $null -and $PropertyTypes[$p.Name].TypeConstraint -eq 'Instance')
{
$errorMessage = $LocalizedData.ConvertValueToPropertyFailed -f @('$null', $Type, $p.Name, $ResourceName)
$errorMessage += Get-PositionInfo $Properties['SourceInfo']
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidArgument -ErrorId FailToProcessProperty
Update-ConfigurationErrorCount
}
if($p.Value -is [PSCredential])
{
[bool] $PSDscAllowPlainTextPassword = $false
[bool] $PSDscAllowDomainUser = $false
$key = 'HKLM:\SOFTWARE\Microsoft\PowerShell\3\DSC'
$reg = Get-ItemProperty -path $key -name "PSDscAllowPlainTextPassword" -ea SilentlyContinue
if ($reg -ne $null -and $reg.PSDscAllowPlainTextPassword -ne $null)
{
$PSDscAllowPlainTextPassword = ((Get-ItemProperty -Path $key -Name PSDscAllowPlainTextPassword).PSDscAllowPlainTextPassword -eq "True")
}
$reg = Get-ItemProperty -path $key -name "PSDscAllowDomainUser" -ea SilentlyContinue
if ($reg -ne $null -and $reg.PSDscAllowDomainUser -ne $null)
{
$PSDscAllowDomainUser = ((Get-ItemProperty -Path $key -Name PSDscAllowDomainUser).PSDscAllowDomainUser -eq "True")
}
[bool] $PSDscDomainUser = IsDomainUser -username $p.Value.UserName
if($Node -and $selectedNodesData)
{
if($selectedNodesData -is [array])
{
foreach($target in $selectedNodesData)
{
if($target['NodeName'] -and $target['NodeName'] -eq $Node)
{
$currentNode = $target
}
}
}
else
{
$currentNode = $selectedNodesData
}
}
# where user need to specify properties for resources not in a node,
# they can do it through localhost nodeName in $allNodes
elseif($allnodes -and $allnodes.AllNodes)
{
foreach($target in $allnodes.AllNodes)
{
if($target['NodeName'] -and $target['NodeName'] -eq 'localhost')
{
$currentNode = $target
}
}
}
if($currentNode)
{
# PSDscAllowDomainUser set to true would indicate that we want to allow
# domain credential. It takes precedence over PSDscAllowPlainTextPassword behavior
if($currentNode['PSDscAllowDomainUser'])
{
$PSDscAllowDomainUser = $currentNode['PSDscAllowDomainUser']
}
if($PSDscDomainUser -and (-not $PSDscAllowDomainUser))
{
if($Script:NodeUsingDomainCred -eq $null)
{
$Script:NodeUsingDomainCred = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,bool]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
$Script:NodeUsingDomainCred[$currentNode['NodeName']] = $true
}
# PSDscAllowPlainTextPassword set to true would indicate that we want to allow the
# automatic conversion of the encrypted password to plaintext if a certificate or
# certificate id is not specified.
# if a certid or cert file is specified, it taks precedence over PSDscAllowPlainTextPassword
if($currentNode['PSDscAllowPlainTextPassword'])
{
$PSDscAllowPlainTextPassword = $currentNode['PSDscAllowPlainTextPassword']
}
$certificateid = $currentNode['CertificateID']
if ( -not $certificateid)
{
# CertificateFile is the public key file
$certificatefile = $currentNode['CertificateFile']
if (( -not $certificatefile) -and (-not $PSDscAllowPlainTextPassword))
{
$errorMessage = $($LocalizedData.EncryptedToPlaintextNotAllowed)
ThrowError -ExceptionName 'System.InvalidOperationException' -ExceptionMessage $errorMessage -ExceptionObject $p -ErrorId 'InvalidPathSpecified' -ErrorCategory InvalidOperation
}
}
if($currentNode['NodeName'] -and ($certificatefile -or $certificateid))
{
$Script:NodesPasswordEncrypted[$currentNode['NodeName']] = $true
}
$p.Name + ' = ' + (stringify -value $p.Value -asArray $asArray -targetType $targetType ) + ";`n"
}
else
{
$errorMessage = $($LocalizedData.EncryptedToPlaintextNotAllowed)
ThrowError -ExceptionName 'System.InvalidOperationException' -ExceptionMessage $errorMessage -ExceptionObject $p -ErrorId 'InvalidPathSpecified' -ErrorCategory InvalidOperation
}
}
else
{
if ($targetTypeName -match 'StringArray')
{
$p.Name + ' = ' + (stringify -value $p.Value -asArray $asArray -targetType $targetType ) + ";`n"
}
elseif ($targetTypeName -notmatch 'Array' -or $p.Value.Count)
{
$p.Name + ' = ' + (stringify -value $p.Value -asArray $asArray -targetType $targetType ) + ";`n"
}
}
}
}
}
else
{
if ($Type -notmatch 'OMI_ConfigurationDocument')
{
$result += "// This instance definition of $Type had no properties`n"
}
Write-Debug -Message " ENCOUNTERED INSTANCE OF TYPE '$Type' WITH NO PROPERTIES"
}
}
catch
{
$errorMessage = $_.Exception.Message + (Get-PositionInfo $Properties['SourceInfo'])
$errorMessage = $LocalizedData.FailToProcessProperty -f @($_.Exception.GetType().FullName, $p.Name, $Type, $errorMessage)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId FailToProcessProperty
Update-ConfigurationErrorCount
}
finally
{
$OFS = $oldOFS
}
#
# Add extra information about Author, GenerationHost, GenerationDate and Name if they are not specified
#
if ($Type -match 'OMI_ConfigurationDocument' -and $Properties)
{
if (-not $Properties.ContainsKey('Author'))
{
$result += " Author = `"$([system.environment]::UserName)`";`n"
}
if (-not $Properties.ContainsKey('GenerationDate'))
{
$result += " GenerationDate = `"$(Get-Date)`";`n"
}
if (-not $Properties.ContainsKey('GenerationHost'))
{
$result += " GenerationHost = `"$([system.environment]::MachineName)`";`n"
}
# todo: report error is configuration name does't match
if (-not $Properties.ContainsKey('Name'))
{
$result += " Name = `"$(Get-PSTopConfigurationName)`";`n"
}
}
#
# Append the completed mof instance text to the overall document
#
$instanceText = "`n" + $result + "`n};`n"
#
# Record and return the alias for that document
#
Write-Debug -Message " Added alias $MofAliasString to InstanceAliases array for node '$(Get-PSCurrentConfigurationNode)'"
if ( Get-PSCurrentConfigurationNode )
{
if($Script:NodeInstanceAliases[ (Get-PSCurrentConfigurationNode) ] -eq $null)
{
$Script:NodeInstanceAliases[ (Get-PSCurrentConfigurationNode) ] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeResourceIdAliases[ (Get-PSCurrentConfigurationNode) ] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
$Script:NodeInstanceAliases[ (Get-PSCurrentConfigurationNode) ][$MofAliasString] = $instanceText
if($Properties.ContainsKey('ResourceID'))
{
$Script:NodeResourceIdAliases[ (Get-PSCurrentConfigurationNode) ][$Properties['ResourceID']] = $MofAliasString
}
}
else
{
$Script:NoNameNodeInstanceAliases[$MofAliasString] = $instanceText
if($Properties.ContainsKey('ResourceID'))
{
$Script:NoNameNodeResourceIdAliases[$Properties['ResourceID']] = $MofAliasString
}
}
# todo: we can check error for duplicated alias in a node and report it here
# because this can live acrose configurationelement calls
Write-Debug -Message " MOF GENERATION COMPLETED FOR $Type"
$MofAliasString
}
#
#
# Returns the MOF text for the instance that
# corresponds to the aliasId
#
function Get-MofInstanceText
{
param (
[Parameter(Mandatory)]
[string]
$aliasId
)
if ( Get-PSCurrentConfigurationNode )
{
$Script:NodeInstanceAliases[ (Get-PSCurrentConfigurationNode) ][$aliasId]
}
else
{
$Script:NoNameNodeInstanceAliases[$aliasId]
}
}
#
# Get the position message based on the SourceMetadata
#
function Get-PositionInfo
{
param(
[string]
$sourceMetadata
)
$positionMessage = ''
if ($sourceMetadata)
{
$positionMessage = "`nAt"
$infoItems = $sourceMetadata -split '::'
# File name may be empty
if ($infoItems[0])
{
$positionMessage += " $($infoItems[0]):$($infoItems[1])"
}
else
{
$positionMessage += " line:$($infoItems[1])"
}
$positionMessage += " char:$($infoItems[2])"
$positionMessage += "`n+ $($infoItems[3])"
}
$positionMessage
}
###################################################################################
#
# A function that implements the 'Node' keyword logic. The Node keyword accumulates
# the resource instances to define for a specific node or nodes. It's passed into the
# configuration statement using the ScriptBlock.InvokeWithContext() method
#
function Node
{
[OutputType([void])]
param (
[Parameter(Mandatory)]
$KeywordData, # Not used in this function....
[string[]]
$Name, # the list of nodes to process in this call
[Parameter(Mandatory)]
[ScriptBlock]
$Value, # The scriptblock that generates the configuration in each node.
[Parameter(Mandatory)]
$sourceMetadata # Not used in this function
)
if (-not $Name)
{
Write-Debug -Message 'The name parameter was empty, no nodes generated'
return
}
Write-Debug -Message "*PROCESSING STARTED FOR NODE SET {$(@($Name) -join ',')} "
# Save any global level resources and initialize for the resources defined for this node.
$Script:PSOuterConfigurationNodes.Push( (Get-PSCurrentConfigurationNode) )
$OldNodeResources = $Script:NodeResources
$OldNodeKeys = $Script:NodeKeys
try
{
$OuterNode = $Script:PSOuterConfigurationNodes.Peek()
if(( $OuterNode -eq [string]::Empty) -or
(($OuterNode -ne [string]::Empty) -and ($Name -contains $OuterNode)))
{
#
# Set up a map from node name to data
#
$nodeDataMap = @{}
#
# Copy the AllNodes data into the map
#
if($script:ConfigurationData)
{
$script:ConfigurationData.AllNodes | ForEach-Object -Process {
$nodeDataMap[$_.NodeName] = $_
}
}
#
# Create the SelectedNodes list for this Node statement
#
$selectedNodesData = foreach ($nn in $Name)
{
# If there is no data for this node, create a dummy node
# with at least the node name
if ( -not $nodeDataMap[$nn] )
{
$nodeDataMap[$nn] = @{
NodeName = $nn
}
}
$nodeDataMap[$nn]
}
foreach ($Node in $Name)
{
if(($OuterNode -ne [string]::Empty) -and ($OuterNode -ne $Node))
{
continue
}
Set-PSCurrentConfigurationNode $Node
if( $Script:NodesInThisConfiguration[$Node] )
{
$Script:NodeResources = $Script:NodesInThisConfiguration[$Node]
}
else
{
$Script:NodesInThisConfiguration[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,[string[]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeResources = $Script:NodesInThisConfiguration[$Node]
}
#this for tracking referenced configuration managers
if( $Script:NodesManagerInThisConfiguration[$Node] )
{
$Script:NodeManager = $Script:NodesManagerInThisConfiguration[$Node]
}
else
{
$Script:NodesManagerInThisConfiguration[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeManager = $Script:NodesManagerInThisConfiguration[$Node]
}
#this for tracking exclusive resources
if( $Script:NodesExclusiveResourcesInThisConfiguration[$Node] )
{
$Script:NodeExclusiveResources = $Script:NodesExclusiveResourcesInThisConfiguration[$Node]
}
else
{
$Script:NodesExclusiveResourcesInThisConfiguration[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeExclusiveResources = $Script:NodesExclusiveResourcesInThisConfiguration[$Node]
}
#this for tracking referenced Resource Module Source
if( $Script:NodesResourceSourceInThisConfiguration[$Node] )
{
$Script:NodeResourceSource = $Script:NodesResourceSourceInThisConfiguration[$Node]
}
else
{
$Script:NodesResourceSourceInThisConfiguration[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeResourceSource = $Script:NodesResourceSourceInThisConfiguration[$Node]
}
if(-not $Script:NodeTypeRefCount[$Node])
{
$Script:NodeTypeRefCount[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, System.Collections.Generic.Dictionary[string,int]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeInstanceAliases[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, System.Collections.Generic.Dictionary[string,string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeResourceIdAliases[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, System.Collections.Generic.Dictionary[string,string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
if(-not $script:NodesKeysInThisConfiguration[$Node])
{
$script:NodesKeysInThisConfiguration[$Node] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.HashSet[string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
$Script:NodeKeys = $script:NodesKeysInThisConfiguration[$Node]
#
# Set up the context variable list.
#
$variablesToDefine = @(
New-Object -TypeName PSVariable -ArgumentList ('SelectedNodes', $selectedNodesData)
New-Object -TypeName PSVariable -ArgumentList ('Node', $nodeDataMap[$Node])
New-Object -TypeName PSVariable -ArgumentList ('NodeName', $Node)
)
# Initialize dictionary to detect duplicate resources
if ($Script:DuplicateResources -eq $null)
{
$Script:DuplicateResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[System.Collections.Hashtable]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
if (-not $Script:DuplicateResources.ContainsKey($Name))
{
$Script:DuplicateResources[$Name] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[System.Collections.Hashtable]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
try
{
# Evaluate the node's business logic scriptblock
Write-Debug -Message "*$Node : NODE PROCESSING STARTED FOR THIS NODE."
$Value.InvokeWithContext($functionsToDefine, $variablesToDefine)
}
catch [System.Management.Automation.MethodInvocationException]
{
# Write the unwrapped exception message
$pscmdlet.CommandRuntime.WriteError((Get-InnerMostErrorRecord $_))
Update-ConfigurationErrorCount
}
# Display warning if domain credential is used in the configuration.
if($Script:NodeUsingDomainCred.ContainsKey($Node) -and $Script:NodeUsingDomainCred[$Node])
{
$nameMessage = $LocalizedData.DomainCredentialNotAllowed -f @($Node, $Node)
$UserWarningPreference = $WarningPreference
if($ArgsToBody['WarningAction'] -ne $null)
{
$UserWarningPreference = $ArgsToBody['WarningAction']
}
Write-Warning -Message $nameMessage -WarningAction $UserWarningPreference
}
# Validate make sure all of the required resources are defined
# if so, add the DependsOn fields for all resources
ValidateNodeResources
#
# Fixup ModuleVersion
#
Update-ModuleVersion $Script:NodeResources $Script:NodeInstanceAliases[$Node] $Script:NodeResourceIdAliases[$Node]
# Validate make sure all of the referenced download managers are defined
ValidateNodeManager
# Validate make sure all of the referenced resource source are defined
ValidateNodeResourceSource
# Validate make sure no conflict between exclusive resources
# also validate exclusive resource exist
ValidateNodeExclusiveResources
# Validate make sure all of the required resources are defined
ValidateNoCircleInNodeResources
Write-Debug -Message "*$Node : NODE PROCESSING COMPLETED FOR THIS NODE. configuration errors encountered so far: $(Get-ConfigurationErrorCount)"
}
}
else
{
$errorMessage = $LocalizedData.NestedNodeNotAllowed -f @(($Name -join ','), (Get-PSCurrentConfigurationNode), $Script:PSConfigurationName)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId NestedNodeNotAllowed
Update-ConfigurationErrorCount
}
}
catch
{
$errorMessage = $_.Exception.Message + (Get-PositionInfo $sourceMetadata)
$errorMessage = $LocalizedData.FailToProcessNode -f @($Node, $errorMessage)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId FailToProcessNode
Update-ConfigurationErrorCount
}
finally
{
Set-PSCurrentConfigurationNode ($Script:PSOuterConfigurationNodes.Pop())
$Script:NodeResources = $OldNodeResources
$Script:NodeKeys = $OldNodeKeys
Write-Debug -Message "*NODE STATEMENT PROCESSING COMPLETED. Configuration errors encountered so far: $(Get-ConfigurationErrorCount)"
}
}
#
# Utility used to track the number of errors encountered while processing the configuration
#
function Update-ConfigurationErrorCount
{
[OutputType([void])]
param()
$Script:PSConfigurationErrors++
}
function Get-ConfigurationErrorCount
{
[OutputType([int])]
param()
$Script:PSConfigurationErrors
}
function Get-ComplexResourceQualifier
{
[OutputType([string])]
param()
# walk the call stack to get at all of the enclosing configuration resource IDs
$stackedConfigs = @(Get-PSCallStack |
where { ($_.InvocationInfo.MyCommand -ne $null) -and ($_.InvocationInfo.MyCommand.CommandType -eq 'Configuration') })
$complexResourceQualifier = $null
# keep all but the top-most
if(@($stackedConfigs).Length -ge 3)
{
$stackedConfigs = $stackedConfigs[1..(@($stackedConfigs).Length - 2)]
# and build the complex resource ID suffix.
$complexResourceQualifier = ( $stackedConfigs | foreach { '[' + $_.Command + ']' + $_.InvocationInfo.BoundParameters['InstanceName'] } ) -join '::'
}
return $complexResourceQualifier
}
#
# A function to set the document text for default (unnamed) node
#
function Set-PSDefaultConfigurationDocument
{
[OutputType([void])]
param (
[Parameter()]
[string]
$documentText = ''
)
$Script:PSDefaultConfigurationDocument = $documentText
}
#
# Get the text associated with the default (unnamed) node.
#
function Get-PSDefaultConfigurationDocument
{
[OutputType([string])]
param()
return $Script:PSDefaultConfigurationDocument
}
##################################################
#
# Returns the name of the configuration currently being configured
#
function Get-PSTopConfigurationName
{
[OutputType([string])]
param()
return $Script:PSTopConfigurationName
}
function Set-PSTopConfigurationName
{
[OutputType([void])]
param (
[Parameter()]
[string]
$Name
)
$Script:PSTopConfigurationName = $Name
}
##################################################
#
# Returns the name of the node currently being configured
#
function Get-PSCurrentConfigurationNode
{
[OutputType([string])]
param()
return $Script:PSCurrentConfigurationNode
}
function Set-PSCurrentConfigurationNode
{
[OutputType([void])]
param (
[Parameter()]
[string]
$nodeName
)
$Script:PSCurrentConfigurationNode = $nodeName
}
#################################################
function Set-NodeResources
{
[OutputType([void])]
param (
[Parameter(Mandatory)]
[string]
$resourceId,
[Parameter(Mandatory)]
[AllowNull()]
[AllowEmptyCollection()]
[string[]]
$requiredResourceList
)
if ($Script:NodeResources -eq $null)
{
$Script:NodeResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,[string[]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
$Script:NodeResources[$resourceId] = $requiredResourceList
}
function Test-NodeResources
{
[OutputType([bool])]
param (
[Parameter(Mandatory)]
[string]
$resourceId
)
if ( -not $Script:NodeResources)
{
$false
}
else
{
$Script:NodeResources.ContainsKey($resourceId)
}
}
#
# update a mapping of a partial configuration resource and the managers it referenced
#
function Set-NodeManager
{
[OutputType([void])]
param (
[Parameter(Mandatory)]
[string]
$resourceId,
[Parameter(Mandatory)]
[AllowNull()]
[string[]]
$referencedManagers
)
if ($Script:NodeManager -eq $null)
{
$Script:NodeManager = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
$Script:NodeManager[$resourceId] = $referencedManagers
}
#
#
#
function Test-NodeManager
{
[OutputType([bool])]
param (
[Parameter(Mandatory)]
[string]
$resourceId
)
if ( -not $Script:NodeManager)
{
$false
}
else
{
$Script:NodeManager.ContainsKey($resourceId)
}
}
#
# update a mapping of a partial configuration resource and the managers it referenced
#
function Set-NodeResourceSource
{
[OutputType([void])]
param (
[Parameter(Mandatory)]
[string]
$resourceId,
[Parameter(Mandatory)]
[AllowNull()]
[string[]]
$referencedResourceSources
)
if ($Script:NodeResourceSource -eq $null)
{
$Script:NodeResourceSource = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
$Script:NodeResourceSource[$resourceId] = $referencedResourceSources
}
#
#
#
function Test-NodeResourceSource
{
[OutputType([bool])]
param (
[Parameter(Mandatory)]
[string]
$resourceId
)
if ( -not $Script:NodeResourceSource)
{
$false
}
else
{
$Script:NodeResourceSource.ContainsKey($resourceId)
}
}
#
# update a mapping of a partial configuration resource and the managers it referenced
# resource format can be moduleName\* moduleName\resourceName and resourceName
# this is validated during mof generation/cananic stage
#
function Set-NodeExclusiveResources
{
[OutputType([string[]])]
param (
[Parameter(Mandatory)]
[string]
$resourceId,
[Parameter(Mandatory)]
[AllowNull()]
[string[]]
$exclusiveResource
)
$Script:NodeExclusiveResources[$resourceId] = $exclusiveResource
return $exclusiveResource
}
function Add-NodeKeys
{
[OutputType([void])]
param (
[Parameter(Mandatory)]
[string]
$ResourceKey,
[parameter(Mandatory)]
[string]
$keywordName
)
if ($Script:NodeKeys -eq $null)
{
$Script:NodeKeys = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.HashSet[string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
if( -not $Script:NodeKeys.ContainsKey($keywordName))
{
$Script:NodeKeys[$keywordName] = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
if(-not $Script:NodeKeys[$keywordName].Contains($ResourceKey))
{
$null = $Script:NodeKeys[$keywordName].Add($ResourceKey)
}
}
###########################################################
#
# A function to verify there's no duplicate conflicting resources in the configuration
#
# Conflict detecting algorithm works by going through all previuosly visited resources of same type and checking whether previously visited
# resource contains any properties which currently analyzed resource does not have or whether
# they have same properties but with different values. If that's the case, we mark that either key or non-key properties don't match.
# After that we check whether currently analyzed resource contains properties which the previously analyzed resource did not have at all. If that's the case we mark that
# non-key properties don't match (since all key properties were covered in the first phase).
#
# Once we processed all previous resources, we return error about duplicate conflicting resources if and only if key properties match and non key properties don't match.
#
###########################################################
function Test-ConflictingResources
{
[OutputType([void])]
param (
[string]
$keyword,
[parameter(Mandatory)]
[Hashtable]
$properties,
[Parameter(Mandatory)]
$keywordData
)
$resourceID = $properties['ResourceID']
$sourceInfo = $properties['SourceInfo']
$nonConflictingKeywords = @("PartialConfiguration", "WaitForAny", "WaitForSome", "WaitForAll")
if ($keyword -in $nonConflictingKeywords)
{
return
}
# Get name of the current node
$currentNodeName = Get-PSCurrentConfigurationNode
if (-not $currentNodeName)
{
$currentNodeName = 'localhost'
}
# Initialize $Script:DuplicateResources if not already initialized
if ($Script:DuplicateResources -eq $null)
{
$Script:DuplicateResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[System.Collections.Hashtable]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
if (-not $Script:DuplicateResources.ContainsKey($currentNodeName))
{
$Script:DuplicateResources[$currentNodeName] = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[System.Collections.Hashtable]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
}
if ( -not $Script:DuplicateResources[$currentNodeName].ContainsKey($keyword))
{
$Script:DuplicateResources[$currentNodeName][$keyword] = New-Object 'System.Collections.Generic.List[System.Collections.Hashtable]'
}
# Find if current resource is duplicate and conflicting.
foreach($resource in $Script:DuplicateResources[$currentNodeName][$keyword])
{
$keyPropertiesMatch = $true
$nonKeyPropertiesMatch = $true
$unmatchedNonKeyPropertiesNames = ""
$unmatchedNonKeyPropertiesCurrentValues = ""
$unmatchedNonKeyPropertiesPreviousValues = ""
# For every property in previously analyzed resource
foreach($property in $resource.Keys)
{
$nonConflictingProperties = @("DependsOn", "ResourceID", "SourceInfo", "ModuleVersion")
if ($property -in $nonConflictingProperties)
{
continue
}
# If currently analyzed resource does not have the property
if ( -not $properties.ContainsKey($property))
{
$nonKeyPropertiesMatch = $false
$unmatchedNonKeyPropertiesNames += $property.ToString() + ';'
# Calling ToString() explicitly so that the value is not converted to Boolean/Integer
if ($resource[$property])
{
$unmatchedNonKeyPropertiesPreviousValues += $resource[$property].ToString() + ';'
}
else
{
$unmatchedNonKeyPropertiesPreviousValues = $unmatchedNonKeyPropertiesPreviousValues.ToString() + 'NULL;'
}
$unmatchedNonKeyPropertiesCurrentValues = $unmatchedNonKeyPropertiesCurrentValues.ToString() + 'NULL;'
}
# If currently analyzed resource has property, but with different value
elseif ( $resource[$property] -ne $properties[$property] )
{
# If it's a key property
if ($keywordData.Properties[$property].IsKey)
{
$keyPropertiesMatch = $false
break
}
# If it's a non-key property
else
{
$nonKeyPropertiesMatch = $false
$unmatchedNonKeyPropertiesNames += $property.ToString() + ';'
if ($resource[$property])
{
$unmatchedNonKeyPropertiesPreviousValues += $resource[$property].ToString() + ';'
}
else
{
$unmatchedNonKeyPropertiesPreviousValues = $unmatchedNonKeyPropertiesPreviousValues.ToString() + 'NULL;'
}
if ($properties[$property])
{
$unmatchedNonKeyPropertiesCurrentValues += $properties[$property].ToString() + ';'
}
else
{
$unmatchedNonKeyPropertiesCurrentValues = $unmatchedNonKeyPropertiesCurrentValues.ToString() + 'NULL;'
}
}
}
}
if ($keyPropertiesMatch)
{
foreach($property in $properties.Keys)
{
# If previously analyzed resource does not have property
if ((-not $resource.containsKey($property)) -and ($property -ne "ModuleVersion"))
{
$nonKeyPropertiesMatch = $false
$unmatchedNonKeyPropertiesNames += $property.ToString() + ';'
$unmatchedNonKeyPropertiesPreviousValues = $unmatchedNonKeyPropertiesPreviousValues.ToString() + 'NULL;'
if ($properties[$property])
{
$unmatchedNonKeyPropertiesCurrentValues += $properties[$property].ToString() + ';'
}
else
{
$unmatchedNonKeyPropertiesCurrentValues = $unmatchedNonKeyPropertiesCurrentValues.ToString() + 'NULL;'
}
}
}
}
# If resource is duplicate and conflicting we should return error
if ($keyPropertiesMatch -and (-not $nonKeyPropertiesMatch))
{
$unmatchedNonKeyPropertiesNames = $unmatchedNonKeyPropertiesNames.Substring(0, $unmatchedNonKeyPropertiesNames.Length-1)
$unmatchedNonKeyPropertiesPreviousValues = $unmatchedNonKeyPropertiesPreviousValues.Substring(0, $unmatchedNonKeyPropertiesPreviousValues.Length-1)
$unmatchedNonKeyPropertiesCurrentValues = $unmatchedNonKeyPropertiesCurrentValues.Substring(0, $unmatchedNonKeyPropertiesCurrentValues.Length-1)
$previousResourceIdentifier = $resource["ResourceID"] + " (" + $resource["SourceInfo"] + ")"
$currentResourceIdentifier = $resourceID + " (" + $sourceInfo + ")"
$errorMessage = $LocalizedData.ConflictingDuplicateResource -f @($previousResourceIdentifier, $currentResourceIdentifier, $currentNodeName, $unmatchedNonKeyPropertiesNames, $unmatchedNonKeyPropertiesPreviousValues, $unmatchedNonKeyPropertiesCurrentValues)
$exception = New-Object System.InvalidOperationException $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConflictingDuplicateResource
Update-ConfigurationErrorCount
}
}
$Script:DuplicateResources[$currentNodeName][$keyword].Add($properties)
}
#################################################
#
# A function to get the innermost error record.
#
function Get-InnerMostErrorRecord
{
param (
[Parameter(Mandatory)]
[System.Management.Automation.ErrorRecord]
$ErrorRecord
)
$exception = $ErrorRecord.Exception
while ($exception.InnerException -and $exception.InnerException.ErrorRecord)
{
$exception = $exception.InnerException
$ErrorRecord = $exception.ErrorRecord
}
$ErrorRecord
}
###########################################################
#
# A function to set up all of the module-scoped state variables.
#
function Initialize-ConfigurationRuntimeState
{
param (
[Parameter()]
[string]
$ConfigurationName = ''
)
# The overall name of the configuration being processed
[string] $Script:PSConfigurationName = $ConfigurationName
# The ModuleVersion used for PsDesiredStateConfiguration module.
[string] $Script:PsDscModuleVersion = "0.0"
# The compatibility version for the document..
[string] $Script:PsDscCompatibleVersion = "1.0.0"
# The list of modules explicitly imported using import-dscresorce
$Script:ExplicitlyImportedModules = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,String]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the current configuration, this contains the name of the node currently being processed
[string] $Script:PSCurrentConfigurationNode = ''
# For the current node, this contains a map of resource instance to resource prerequisites (DependsOn resources).
[System.Collections.Generic.Dictionary[string,string[]]] `
$Script:NodeResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,[string[]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the current node, this contains a map of partial configuration instance to configuration managers.
[System.Collections.Generic.Dictionary[string,string]] `
$Script:NodeManager = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the current node, this contains a map of partial configuration instance to Resource Source.
[System.Collections.Generic.Dictionary[string,string]] `
$Script:NodeResourceSource = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the current node, this contains a map of partial configuration instance to its exclusive resources.
[System.Collections.Generic.Dictionary[string,string[]]] `
$Script:NodeExclusiveResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes except the unnamed node, this contains the per-node per-type reference counter used to generate the CIM aliases for each instance
[System.Collections.Generic.Dictionary[string, System.Collections.Generic.Dictionary[string,int]]] `
$Script:NodeTypeRefCount = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, System.Collections.Generic.Dictionary[string,int]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes except the unnamed node, this maps the node name to the node's table of alias to mof text mappings.
[System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,string]]] `
$Script:NodeInstanceAliases = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes except the unnamed node, this maps the node name to the node's table of resourceID to mof text mappings.
[System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,string]]] `
$Script:NodeResourceIdAliases = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes except the unnamed node, this maps the node name to the node's map of resource instance to resource prerequisites
[System.Collections.Generic.Dictionary[string,object]] `
$Script:NodesInThisConfiguration = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,object]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes except the unnamed node, this maps the node name to the node's map of partial configuration resource instance to reference resource source.
[System.Collections.Generic.Dictionary[string,object]] `
$Script:NodesResourceSourceInThisConfiguration = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,object]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes except the unnamed node, this maps the node name to the node's map of partial configuration resource instance to reference configuraiton manager.
[System.Collections.Generic.Dictionary[string,object]] `
$Script:NodesManagerInThisConfiguration = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,object]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes except the unnamed node, this maps the node name to the node's map of resource instance to exclusive resource
[System.Collections.Generic.Dictionary[string,object]] `
$Script:NodesExclusiveResourcesInThisConfiguration = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,object]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this contains a map of resource instance to resource prerequisites (required resources).
[System.Collections.Generic.Dictionary[String,String[]]] `
$Script:NoNameNodesResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,String[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this contains a map of partial configuration resource instance to reference configuration manager.
[System.Collections.Generic.Dictionary[String,String]] `
$Script:NoNameNodeManager = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,String[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this contains a map of partial configuration resource instance to reference resource source.
[System.Collections.Generic.Dictionary[String,String]] `
$Script:NoNameNodeResourceSource = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,String[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this contains a map of partial configuration resource instance to its exclusive resources.
[System.Collections.Generic.Dictionary[String,String[]]] `
$Script:NoNameNodeExclusiveResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,String[]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this contains the per-node per-type reference counter used to generate the CIM aliases for each instance
[System.Collections.Generic.Dictionary[String,int]] `
$Script:NoNameNodeTypeRefCount = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,int]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this maps the node name to the node's table of alias to mof text mappings.
# Alias to mof text mapping for the unnamed node.
[System.Collections.Generic.Dictionary[string,string]] `
$Script:NoNameNodeInstanceAliases = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this maps the node name to the node's table of resourceId to mof text mappings.
# Alias to mof text mapping for the unnamed node.
[System.Collections.Generic.Dictionary[string,string]] `
$Script:NoNameNodeResourceIdAliases = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
#dictionary to save whether a node has encrypted password
[System.Collections.Generic.Dictionary[string,bool]] `
$Script:NodesPasswordEncrypted = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,bool]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For all nodes, contain information about node using domain credential or not
[System.Collections.Generic.Dictionary[string,bool]] `
$Script:NodeUsingDomainCred = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,bool]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
[System.Collections.Generic.Stack[String]] $Script:PSOuterConfigurationNodes = New-Object -TypeName 'System.Collections.Generic.Stack[String]'
# The number of errors that have occurred while processing this configuration.
[int] $Script:PSConfigurationErrors = 0
# Set up a variable to hold a top-level OMI_ConfigurationDocument value
# which will be used by all nodes if it's present.
[string] $Script:PSDefaultConfigurationDocument = ''
# Set up a hastable to hold user specified info for OMI_ConfigurationDocument value for meta config
# we will need update it last after processing meta config to figure out what V2 property it uses
[hashtable] $script:PSMetaConfigDocumentInstVersionInfo = @{}
[bool] $Script:PSMetaConfigDocInsProcessedBeforeMeta = $false
[bool] $script:PSMetaConfigurationProcessed = $false
# For all nodes except the unnamed node, this maps the node name to the node's map of keys of resources.
[System.Collections.Generic.Dictionary[string,object]] `
$script:NodesKeysInThisConfiguration = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,object]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the current node, this contains keys of resource instances.
[System.Collections.Generic.Dictionary[string,System.Collections.Generic.HashSet[string]]] `
$Script:NodeKeys = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.HashSet[string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the unnamed (default) node, this contains keys of resource instances.
[System.Collections.Generic.Dictionary[String,System.Collections.Generic.HashSet[string]]] `
$Script:NoNameNodeKeys = New-Object -TypeName 'System.Collections.Generic.Dictionary[String,System.Collections.Generic.HashSet[string]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# For the current configuration, $Script:DuplicateResources["Type"] contains list with properties of all resources of the specific type
[System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[System.Collections.Hashtable]]]] `
$Script:DuplicateResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,System.Collections.Generic.Dictionary[string,System.Collections.Generic.List[System.Collections.Hashtable]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
# Show Import-DscResource warning if in-build resources are used in the Configuration without Import-DscResource statement.
[bool] $script:ShowImportDscResourceWarning = $false
# For testing whether setting meta configuration into PULL mode without appropriate configuration manager
[bool] $script:MetaConfigSetToPull = $false
[bool] $script:SpecifiedV1DownloadManagerName = $false
[bool] $script:SpecifiedV2DownloadManager = $false
}
#
# Then call it to enact the default state. This happens
# only once during the module load. Thereafter the
# configuration function is responsible for resetting state.
#
Initialize-ConfigurationRuntimeState
# make sure configuration data format:
# 1. Is a hashtable
# 2. Has a collection AllNodes
# 3. Allnodes is an collection of Hashtable
# 4. Each element of Allnodes has NodeName
# 5. We will copy values from NodeName="*" to all node if they don't exist
function ValidateUpdate-ConfigurationData
{
param (
[Parameter()]
[hashtable]
$ConfigurationData
)
if( -not $ConfigurationData.ContainsKey('AllNodes'))
{
$errorMessage = $LocalizedData.ConfiguratonDataNeedAllNodes
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConfiguratonDataNeedAllNodes
return $false
}
if($ConfigurationData.AllNodes -isnot [array])
{
$errorMessage = $LocalizedData.ConfiguratonDataAllNodesNeedHashtable
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConfiguratonDataAllNodesNeedHashtable
return $false
}
$nodeNames = New-Object -TypeName 'System.Collections.Generic.HashSet[string]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
foreach($Node in $ConfigurationData.AllNodes)
{
if($Node -isnot [hashtable] -or -not $Node.NodeName)
{
$errorMessage = $LocalizedData.AllNodeNeedToBeHashtable
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConfiguratonDataAllNodesNeedHashtable
return $false
}
if($nodeNames.Contains($Node.NodeName))
{
$errorMessage = $LocalizedData.DuplicatedNodeInConfigurationData -f $Node.NodeName
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId DuplicatedNodeInConfigurationData
return $false
}
if($Node.NodeName -eq '*')
{
$AllNodeSettings = $Node
}
[void] $nodeNames.Add($Node.NodeName)
}
if($AllNodeSettings)
{
foreach($Node in $ConfigurationData.AllNodes)
{
if($Node.NodeName -ne '*')
{
foreach($nodeKey in $AllNodeSettings.Keys)
{
if(-not $Node.ContainsKey($nodeKey))
{
$Node.Add($nodeKey, $AllNodeSettings[$nodeKey])
}
}
}
}
$ConfigurationData.AllNodes = @($ConfigurationData.AllNodes | Where-Object -FilterScript {
$_.NodeName -ne '*'
}
)
}
return $true
}
##############################################################
#
# Checks to see if a module defining composite resources should be reloaded
# based the last write time of the schema file. Returns true if the file exists
# and the last modified time was either not recorded or has change.
#
function Test-ModuleReloadRequired
{
[OutputType([bool])]
param (
[Parameter(Mandatory)]
[string]
$SchemaFilePath
)
if (-not $SchemaFilePath -or $SchemaFilePath -notmatch '\.schema\.psm1$')
{
# not a composite res
return $false
}
# If the path doesn't exist, then we can't reload it.
# Note: this condition is explicitly not an error for this function.
if ( -not (Test-Path $SchemaFilePath))
{
if ($schemaFileLastUpdate.ContainsKey($SchemaFilePath))
{
$schemaFileLastUpdate.Remove($SchemaFilePath)
}
return $false
}
# If we have a modified date, then return it.
if ($schemaFileLastUpdate.ContainsKey($SchemaFilePath))
{
if ( (Get-Item $SchemaFilePath).LastWriteTime -eq $schemaFileLastUpdate[$SchemaFilePath] )
{
return $false
}
else
{
return $true
}
}
# Otherwise, record the last write time and return true.
$script:schemaFileLastUpdate[$SchemaFilePath] = (Get-Item $SchemaFilePath).LastWriteTime
$true
}
# Holds the schema file to lastwritetime mapping.
[System.Collections.Generic.Dictionary[string,DateTime]] $script:schemaFileLastUpdate =
New-Object -TypeName 'System.Collections.Generic.Dictionary[string,datetime]'
###########################################################
# Configuration keyword implementation
###########################################################
#
# Implements the 'configuration' keyword logic that accumulates and writes
# out the generated DSC MOF files
#
function Configuration
{
[CmdletBinding(HelpUri = 'http://go.microsoft.com/fwlink/?LinkId=517195')]
param (
# there are extra [] around Tuple arguments
[System.Collections.Generic.List[System.Tuple[[string[]], [Microsoft.PowerShell.Commands.ModuleSpecification[]], [Version]]]]
$ResourceModuleTuplesToImport,
$OutputPath = '.',
$Name,
[scriptblock]
$Body,
[hashtable]
$ArgsToBody,
[hashtable]
$ConfigurationData = @{
AllNodes = @()
},
[string]
$InstanceName = ''
)
function ConvertModuleDefnitionToModuleInfo
{
param(
[Microsoft.PowerShell.Commands.ModuleSpecification[]]$moduleToImport,
[Version]$moduleVersion = $null
)
if (-not $moduleToImport) {
return $null
}
$moduleToImport | % {
$versionToUse = $_.Version
if( [string]::IsNullOrEmpty($versionToUse))
{
$versionToUse = $_.RequiredVersion
}
$Script:ExplicitlyImportedModules[ $_.Name ] = $versionToUse
}
$moduleInfos = Get-Module -ListAvailable -FullyQualifiedName $moduleToImport | sort -Property Version -Descending
# Import-DscResource matches exact version of the module if ‘ModuleVersion’ is specified.
# This is intentional, ModuleVersion keyword doesn’t refer to the latest version in Import-DscResource.
# Find the module that matches specified version.
$modules = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Management.Automation.PSModuleInfo]
if (($moduleInfos.Count -gt 0) -and ($moduleToImport.Version -or $moduleToImport.Guid))
{
foreach ($moduleInfo in $moduleInfos)
{
if (($moduleToImport.Guid -and $moduleToImport.Guid.Equals($moduleInfo.Guid.ToString())) -or
($moduleToImport.Version -and $moduleToImport.Version.Equals($moduleInfo.Version)))
{
$modules.Add($moduleInfo)
break
}
}
}
elseif ($moduleInfos.Count -eq 1)
{
$modules.Add($moduleInfos)
}
return $modules
}
try
{
Write-Debug -Message "BEGIN CONFIGURATION '$Name' PROCESSING: OutputPath: '$OutputPath'"
if ($Name -inotmatch '^[a-z][a-z0-9_./-]*$')
{
$errorId = 'InvalidConfigurationName'
$errorMessage = $($LocalizedData.InvalidConfigurationName) -f ${Name}
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId $errorId
}
$script:IsMetaConfig = $false
foreach($attri in $Body.Attributes)
{
if ($attri.GetType() -eq [System.Management.Automation.DscLocalConfigurationManagerAttribute])
{
$script:IsMetaConfig = $true
$Script:PSMetaConfigDocInsProcessedBeforeMeta = $false
$script:PSMetaConfigurationProcessed = $false
break
}
}
# True if this is the top-most level of the configuration statement.
#
[bool] $topLevel = $false
if ($Script:PSConfigurationName -eq '')
{
Write-Debug -Message " $Name : TOP-LEVEL CONFIGURATION STARTED"
$topLevel = $true
Set-PSTopConfigurationName $Name
#
# Define a dictionary to hold the "driver" functions that will be created for each CIM class/keyword.
# This dictionary is passed into the body scriptblock, defining these functions in the body scope
# which simplifies cleanup.
#
$script:functionsToDefine = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,ScriptBlock]'([System.StringComparer]::OrdinalIgnoreCase)
if($ConfigurationData -eq $null)
{
$ConfigurationData = @{
AllNodes = @()
}
}
$dataValidated = ValidateUpdate-ConfigurationData $ConfigurationData
if (-not $dataValidated)
{
Update-ConfigurationErrorCount
Write-Debug -Message 'ConfigurationData validation failed'
return
}
else
{
$script:ConfigurationData = $ConfigurationData
}
if($OutputPath -eq '.' -or $OutputPath -eq $null -or $OutputPath -eq '')
{
$OutputPath = ".\$Name"
}
# Load the default CIM keyword/function definitions set, populating the function collection
# with the default functions.
[Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::LoadDefaultCimKeywords($functionsToDefine)
# Set up the rest of the configuration runtime state.
Initialize-ConfigurationRuntimeState $Name
# Create the output directory only if it does not exist.
# If it exists then the document MOF files will overwrite
# any existing MOF files with the same name but otherwise
# leave contents of existing directory alone.
$ConfigurationOutputDirectory = "$OutputPath"
if (!(Test-Path $ConfigurationOutputDirectory))
{
$mkdirError = $null
mkdir -ErrorVariable mkdirError -Force $ConfigurationOutputDirectory > $null 2> $null
if (! $?)
{
Update-ConfigurationErrorCount
}
if ($mkdirError)
{
$errorId = 'InvalidOutputPath'
$errorCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation
$errorMessage = $($LocalizedData.CannotCreateOutputPath) -f ${ConfigurationOutputDirectory}
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
$ErrorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, $errorId, $errorCategory, $null
Write-Error $ErrorRecord
foreach ($e in $mkdirError)
{
Write-Error -Exception $e.Exception
Update-ConfigurationErrorCount
}
}
}
#
# Add the utility functions used by the resource implementation functions.
#
$functionsToDefine.Add('Get-MofInstanceText', ${function:Get-MofInstanceText} )
$functionsToDefine.Add('ConvertTo-MOFInstance', ${function:ConvertTo-MOFInstance} )
$functionsToDefine.Add('Update-ConfigurationErrorCount', ${function:Update-ConfigurationErrorCount} )
$functionsToDefine.Add('Get-ConfigurationErrorCount', ${function:Get-ConfigurationErrorCount} )
$functionsToDefine.Add('Set-PSDefaultConfigurationDocument', ${function:Set-PSDefaultConfigurationDocument} )
$functionsToDefine.Add('Get-PSDefaultConfigurationDocument', ${function:Get-PSDefaultConfigurationDocument} )
$functionsToDefine.Add('Get-PSCurrentConfigurationNode', ${function:Get-PSCurrentConfigurationNode} )
$functionsToDefine.Add('Set-NodeResources', ${function:Set-NodeResources} )
$functionsToDefine.Add('Test-NodeResources', ${function:Test-NodeResources} )
$functionsToDefine.Add('Set-NodeManager', ${function:Set-NodeManager} )
$functionsToDefine.Add('Test-NodeManager', ${function:Test-NodeManager} )
$functionsToDefine.Add('Set-NodeResourceSource', ${function:Set-NodeResourceSource} )
$functionsToDefine.Add('Test-NodeResourceSource', ${function:Test-NodeResourceSource} )
$functionsToDefine.Add('Set-NodeExclusiveResources', ${function:Set-NodeExclusiveResources} )
$functionsToDefine.Add('Add-NodeKeys', ${function:Add-NodeKeys} )
$functionsToDefine.Add('Test-ConflictingResources', ${function:Test-ConflictingResources} )
$functionsToDefine.Add('Set-PSMetaConfigDocInsProcessedBeforeMeta', ${function:Set-PSMetaConfigDocInsProcessedBeforeMeta} )
$functionsToDefine.Add('Get-PSMetaConfigDocumentInstVersionInfo', ${function:Get-PSMetaConfigDocumentInstVersionInfo} )
$functionsToDefine.Add('Get-PSMetaConfigurationProcessed', ${function:Get-PSMetaConfigurationProcessed} )
$functionsToDefine.Add('Set-PSMetaConfigVersionInfoV2', ${function:Set-PSMetaConfigVersionInfoV2})
#
# Add the node keyword implementation function which must be module qualified even though
# it's not exported from the module because the parsing logic always adds the module name
# to the command call.
#
$functionsToDefine.Add('PSDesiredStateConfiguration\node', ${function:Node})
Write-Debug -Message " $Name : $($functionsToDefine.Count) type handler functions loaded."
Write-Debug -Message " $Name TOP-LEVEL INITIALIZATION COMPLETED"
}
else
{
Write-Debug -Message " $Name : NESTED CONFIGURATION STARTED"
$oldFunctionsToDefine = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,ScriptBlock]'
}
#
# Load all of the required resource definition modules
#
foreach ($tuple in $ResourceModuleTuplesToImport)
{
$res = $tuple.Item1
$modulesInfo = ConvertModuleDefnitionToModuleInfo -moduleToImport $tuple.Item2 -moduleVersion $tuple.Item3
if ($modulesInfo.Count -eq 0) {
# Module name is not specified. Try to load resource from all available modules.
$modulesInfo = Get-Module -ListAvailable
}
foreach ($mod in $modulesInfo) {
try {
$resourcesFound = ImportClassResourcesFromModule -Module $mod -Resources $res -functionsToDefine $functionsToDefine
} catch {
write-error $_
}
$dscResourcesPath = Join-Path -Path $mod.ModuleBase -ChildPath 'DSCResources'
if(Test-Path $dscResourcesPath)
{
foreach($requiredResource in $res)
{
if ($requiredResource.Contains('*')) {
# we historically resolve wildcards by Get-Item File System rules.
# We don't support wildcards resolutions for Friendly names.
foreach ($resource in Get-ChildItem -Path $dscResourcesPath -Directory -Name -Filter $requiredResource)
{
$foundResource = ImportCimAndScriptKeywordsFromModule -Module $mod -Resource $resource -functionsToDefine $functionsToDefine
}
} else {
# ImportCimAndScriptKeywordsFromModule takes care about resolving $requiredResources names to ClassNames or FriendlyNames.
$foundResource = ImportCimAndScriptKeywordsFromModule -Module $mod -Resource $requiredResource -functionsToDefine $functionsToDefine
}
}
}
elseif ($moduleInfos.Count -eq 1)
{
$modules.Add($moduleInfos)
}
}
}
if (-not (Get-PSCurrentConfigurationNode))
{
# A dictionary maps a resourceId to its list of required resources list for any resources
# defined outside of a node statement
$Script:NodeResources = $Script:NoNameNodesResources
$Script:NodeKeys = $Script:NoNameNodeKeys
[System.Collections.Generic.Dictionary[string,string[]]] $OldNodeResources =
New-Object -TypeName 'System.Collections.Generic.Dictionary[string,[string[]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
$Script:NodeManager = $Script:NoNameNodeManager
$Script:NodeExclusiveResources = $Script:NoNameNodeExclusiveResources
$Script:NodeResourceSource = $Script:NoNameNodeResourceSource
}
else
{
$Script:NodeResources = $Script:NodesInThisConfiguration[(Get-PSCurrentConfigurationNode)]
$Script:NodeManager = $Script:NodesManagerInThisConfiguration[(Get-PSCurrentConfigurationNode)]
$Script:NodeResourceSource = $Script:NodesResourceSourceInThisConfiguration[(Get-PSCurrentConfigurationNode)]
$Script:NodeExclusiveResources = $Script:NodesExclusiveResourcesInThisConfiguration[(Get-PSCurrentConfigurationNode)]
}
#
# Evaluate the configuration statement body which will generate the resource definitons
# for this configuration.
#
Write-Debug -Message " $Name : Evaluating configuration statement body..."
try
{
$variablesToDefine = @(
#
# Figure out the "type" of this resource, with is the name of the driver function that was called.
#
New-Object -TypeName PSVariable -ArgumentList ('ConfigurationData', $script:ConfigurationData )
New-Object -TypeName PSVariable -ArgumentList ('MyTypeName', $ExecutionContext.SessionState.Module.GetVariableFromCallersModule('MyInvocation').Value.MyCommand.Name)
New-Object -TypeName PSVariable -ArgumentList ('IsMetaConfig', $script:IsMetaConfig)
New-Object -TypeName PSVariable -ArgumentList ('V1MetaConfigPropertyList', $script:V1MetaConfigPropertyList)
if($script:ConfigurationData)
{
New-Object -TypeName PSVariable -ArgumentList ('AllNodes', $script:ConfigurationData.AllNodes)
}
)
$variablesToDefine += foreach ($key in $ArgsToBody.Keys)
{
#
# we need to process dependsOn seperately to
# 1. combined depends on with possible upper level configuration statement (composite resource case
# 2. in case of 1, we also need to fix up the dependson to append the suffix of ::$complexResourceQualifier similar in func:Test-DependsOn in CimDSCParser
#
# we need to process PsDscRunAsCredential seperately as well
# because it will be set recursively to all composite resources
if(($key -ne 'DependsOn') -and ($key -ne 'PsDscRunAsCredential'))
{
New-Object -TypeName PSVariable -ArgumentList ($key, $ArgsToBody[$key])
}
}
if($DependsOn -or $ArgsToBody['DependsOn'])
{
$complexResourceQualifier = Get-ComplexResourceQualifier
if($complexResourceQualifier)
{
$updatedDependsOn = foreach($DependsOnVar in @($ArgsToBody['DependsOn']))
{
if($DependsOnVar)
{
"$DependsOnVar::$complexResourceQualifier"
}
}
$DependsOn = @($DependsOn) + @($updatedDependsOn)
}
else
{
$DependsOn = @($DependsOn) + @($ArgsToBody['DependsOn'])
}
$variablesToDefine += New-Object -TypeName PSVariable -ArgumentList('DependsOn', $DependsOn)
}
# If PsDscRunAsCredential is specified both in Outer configuration and inner composite resource we give error when merging PsDscRunAsCredential
if($PsDscRunAsCredential -and $ArgsToBody['PsDscRunAsCredential'])
{
Update-ConfigurationErrorCount
Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::PsDscRunAsCredentialMergeErrorForCompositeResources($InstanceName))
}
# If PsDscRunAsCredential is specified in Configuration Argment or in outer configuration as $PsDscRunAsCredential
# set it as a varaible for current configurtaion.
if($PsDscRunAsCredential -or $ArgsToBody['PsDscRunAsCredential'])
{
if($ArgsToBody['PsDscRunAsCredential'])
{
$PsDscRunAsCredential = $ArgsToBody['PsDscRunAsCredential']
}
$variablesToDefine += New-Object -TypeName PSVariable -ArgumentList('PsDscRunAsCredential', $PsDscRunAsCredential)
}
$result = $Body.InvokeWithContext($functionsToDefine, $variablesToDefine)
}
catch [System.Management.Automation.MethodInvocationException]
{
Write-Debug -Message " $Name : Top level exception: $($_ | Out-String)"
# Write the unwrapped exception message
$pscmdlet.CommandRuntime.WriteError((Get-InnerMostErrorRecord $_))
Update-ConfigurationErrorCount
}
#
# write the generated files to disk and return the resulting files to stdout.
#
if( $topLevel )
{
if($Script:NoNameNodeInstanceAliases.Count -gt 0)
{
if ($Script:NodeInstanceAliases.ContainsKey('localhost'))
{
$errorMessage = $LocalizedData.LocalHostNodeNotAllowed -f "$Name"
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId LocalHostNodeNotAllowed
Update-ConfigurationErrorCount
}
#
# Fixup DependsOn
#
ValidateNoNameNodeResources
Update-DependsOn $Script:NoNameNodesResources $Script:NoNameNodeInstanceAliases $Script:NoNameNodeResourceIdAliases
Write-Debug -Message " $Name : Evaluation completed, validating the generated resource set."
ValidateNodeResources
Write-Debug -Message " $Name Validation completed."
#
# Fixup ModuleVersion
#
Update-ModuleVersion $Script:NoNameNodesResources $Script:NoNameNodeInstanceAliases $Script:NoNameNodeResourceIdAliases
Write-Debug -Message " $Name : Evaluation completed, validating the generated resource set has no circle."
ValidateNoCircleInNodeResources
Write-Debug -Message " $Name Validation circle completed."
if($script:IsMetaConfig)
{
# Validate make sure all of the referenced download managers are defined
Write-Debug -Message " $Name : Evaluation completed, validating the generated resource set reference download manager."
ValidateNodeManager
Write-Debug -Message " $Name Validation completed."
# Validate make sure all of the referenced resource source are defined
Write-Debug -Message " $Name : Evaluation completed, validating the generated resource set reference resource source."
ValidateNodeResourceSource
Write-Debug -Message " $Name Validation completed."
# Validate make sure new conflict between exclusive resources
Write-Debug -Message " $Name : Evaluation completed, validating no conflict between exclusive resources."
ValidateNodeExclusiveResources
Write-Debug -Message " $Name Validation completed."
Write-MetaConfigFile $Name 'localhost' $Script:NoNameNodeInstanceAliases
}
else
{
Update-ConfigurationDocumentRef $Script:NoNameNodesResources $Script:NoNameNodeInstanceAliases $Script:NoNameNodeResourceIdAliases $Name
#
# Write the mof instance texts to files
#
Write-NodeMOFFile $Name 'localhost' $Script:NoNameNodeInstanceAliases
}
# If no script-level $ConfigurationData variable is set, this code
# tries to get it first, from a global PowerShell ConfigurationData variable,
# then if that doesn't work it trys the environment
# variable $ENV:ConfigurationData when is expected to contain a JSON string
# that will be converted to objects.
#
if (-not $script:ConfigurationData)
{
$script:ConfigurationData = try
{
if ($global:ConfigurationData)
{
$global:ConfigurationData
}
elseif ($ENV:ConfigurationData)
{
$ENV:ConfigurationData | ConvertFrom-Json
}
else
{
@()
}
}
catch
{
Write-Error $_
Update-ConfigurationErrorCount
@()
}
}
}
if($script:ShowImportDscResourceWarning)
{
$message = $LocalizedData.ImportDscResourceWarningForInbuiltResource -f @(Get-PSTopConfigurationName)
$ImportDscResourceWarningPreference = $WarningPreference
if($ArgsToBody['WarningAction'] -ne $null)
{
$ImportDscResourceWarningPreference = $ArgsToBody['WarningAction']
}
Write-Warning -Message $message -WarningAction $ImportDscResourceWarningPreference
}
#
# write using the top-level hashtable
#
foreach($mofNode in $Script:NodeInstanceAliases.Keys)
{
#
# Fixup DependsOn
#
Update-DependsOn $Script:NodesInThisConfiguration[$mofNode] $Script:NodeInstanceAliases[$mofNode] $Script:NodeResourceIdAliases[$mofNode]
if($script:IsMetaConfig)
{
Write-MetaConfigFile $Name $mofNode $Script:NodeInstanceAliases[$mofNode]
}
else
{
Update-ConfigurationDocumentRef $Script:NodesInThisConfiguration[$mofNode] $Script:NodeInstanceAliases[$mofNode] $Script:NodeResourceIdAliases[$mofNode] $Name
#
# Write the mof instance texts to files
#
Write-NodeMOFFile $Name $mofNode $Script:NodeInstanceAliases[$mofNode]
}
}
if ($Script:PSConfigurationErrors -gt 0)
{
$errorMessage = $LocalizedData.FailToProcessConfiguration -f "$Name"
ThrowError -ExceptionName 'System.InvalidOperationException' -ExceptionMessage $errorMessage -ExceptionObject "$Name" -ErrorId 'FailToProcessConfiguration' -ErrorCategory InvalidOperation
}
}
}
finally
{
if($topLevel)
{
Write-Debug -Message " CONFIGURATION $Name : DOING TOP-LEVEL CLEAN UP"
[System.Management.Automation.Language.DynamicKeyword]::Reset()
[Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ClearCache()
Initialize-ConfigurationRuntimeState
}
Write-Debug -Message "END CONFIGURATION '$Name' PROCESSING. OutputPath: '$OutputPath'"
}
}
Export-ModuleMember -Function Configuration
function Update-ModuleVersion
{
[OutputType([void])]
param(
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[String,String[]]]
$NodeResources,
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[string,string]]
$NodeInstanceAliases,
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[string,string]]
$NodeResourceIdAliases
)
$moduleVersionValue = '0.0'
# explicit import-dscresource with version for psdesiredstateconfiguration module was not done.
if( $Script:ExplicitlyImportedModules.ContainsKey('PsDesiredStateConfiguration') -and
(-not [string]::IsNullOrEmpty($Script:ExplicitlyImportedModules['PsDesiredStateConfiguration'])))
{
$moduleVersionValue= $script:PsDscModuleVersion
}
# generating compatible document.
elseif($script:PsDscCompatibleVersion -eq '1.0.0')
{
$moduleVersionValue='1.0'
}
# The logic is to insert correct moduleVersion for PsDesiredStateConfiguration module. In case of composite
# configuration,determination of what module version to use can happen later. Hence we update all objects
# after processing of every composite configuration to reflect upon with up to date information.
foreach($resourceId in $NodeResources.keys)
{
$alias = $NodeResourceIdAliases[$resourceId]
$instanceText = $NodeInstanceAliases[$alias]
$curlyPosition = $instanceText.LastIndexOf('}')
# check if the resource is from PsDesiredStateConfiguraiton Module.
if(($curlyPosition -gt 0) -and ($instanceText -imatch 'ModuleName[\s]*=[\s]*["]PsDesiredStateConfiguration["]'))
{
$moduleVersionstring = "ModuleVersion = "
$moduleVersionstring += "`"$moduleVersionValue`"" + ";"
# if ModuleVersion field is not there
if($instanceText -inotmatch 'ModuleVersion[\s]*=')
{
$first = $instanceText.Substring(0, $curlyPosition)
$NodeInstanceAliases[$alias] = $first + $moduleVersionstring + "`r`n};"
}
else
{
#match pattern for eg: 'ModuleVersion = "1.0"'
$regexExpression = 'ModuleVersion[\s]*=[\s]*["][\d.]*["][\s]*;'
$NodeInstanceAliases[$alias] = $instanceText -replace $regexExpression, $moduleVersionstring
}
}
}
}
function Update-DependsOn
{
[OutputType([void])]
param(
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[String,String[]]]
$NodeResources,
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[string,string]]
$NodeInstanceAliases,
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[string,string]]
$NodeResourceIdAliases
)
foreach($resourceId in $NodeResources.keys)
{
$alias = $NodeResourceIdAliases[$resourceId]
$needAdd = $false
$instanceText = $NodeInstanceAliases[$alias]
if($NodeResources[$resourceId])
{
$curlyPosition = $instanceText.LastIndexOf('}')
if(($curlyPosition -gt 0) -and ($instanceText -notmatch 'dependsOn[\s]*='))
{
$needAdd = $true
$first = $instanceText.Substring(0, $curlyPosition)
$dependsOn = "DependsOn = {`r`n"
$len = @($NodeResources[$resourceId]).Length
$dependsOn += foreach ($resourceId in $NodeResources[$resourceId])
{
' ' + "`"$($resourceId -replace '\\', '\\' -replace '"', '\"')`"" +
$(if (--$len -gt 0)
{
",`r`n"
}
else
{
''
}
)
}
$dependsOn += '};'
}
}
if($needAdd)
{
$NodeInstanceAliases[$alias] = $first + $dependsOn + "`r`n};"
}
}
}
#
# add a reference to each resource to point to the OMI_ConfigurationDocument
# so it can be differiencated after merging of partical configurations
#
function Update-ConfigurationDocumentRef
{
[OutputType([void])]
param(
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[String,String[]]]
$NodeResources,
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[string,string]]
$NodeInstanceAliases,
[Parameter(Mandatory)]
[System.Collections.Generic.Dictionary[string,string]]
$NodeResourceIdAliases,
[Parameter(Mandatory)]
[string]
$ConfigurationName
)
foreach($resourceId in $NodeResources.keys)
{
$alias = $NodeResourceIdAliases[$resourceId]
$instanceText = $NodeInstanceAliases[$alias]
$needAdd = $false
$curlyPosition = $instanceText.LastIndexOf('}')
if($curlyPosition -gt 0)
{
$needAdd = $true
$first = $instanceText.Substring(0, $curlyPosition).TrimEnd()
$ConfigurationNameRef = "`r`n ConfigurationName = `"$ConfigurationName`";"
}
if($needAdd)
{
$NodeInstanceAliases[$alias] = $first + $ConfigurationNameRef + "`r`n};"
}
}
}
function ImportClassResourcesFromModule
{
param (
[Parameter(Mandatory)]
[PSModuleInfo]
$Module,
[Parameter(Mandatory)]
[System.Collections.Generic.List[string]]
$Resources,
[System.Collections.Generic.Dictionary[string, scriptblock]]
$functionsToDefine
)
$resourceLoadLog = "c:\temp\resourceload.log"
try {
$resourcesFound = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportClassResourcesFromModule($Module, $Resources, $functionsToDefine)
$module | add-content $resourceLoadLog
$resourcesFound | % { ("--- {0}" -f $_) | add-content $resourceLoadLog }
}
catch {
"blows up here at $module" | add-content $resourceLoadLog
write-verbose "$module $resources $functionsToDefine"
}
return ,$resourcesFound
}
function ImportCimAndScriptKeywordsFromModule
{
param (
[Parameter(Mandatory)]
$Module,
[Parameter(Mandatory)]
$resource,
$functionsToDefine
)
trap
{
continue
}
$SchemaFilePath = $null
$oldCount = $functionsToDefine.Count
$keywordErrors = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Exception]'
$foundCimSchema = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportCimKeywordsFromModule(
$Module, $resource, [ref] $SchemaFilePath, $functionsToDefine, $keywordErrors)
foreach($ex in $keywordErrors)
{
Write-Error -Exception $ex
if($ex.InnerException)
{
Write-Error -Exception $ex.InnerException
}
}
$functionsAdded = $functionsToDefine.Count - $oldCount
Write-Debug -Message " $Name : PROCESSING RESOURCE FILE: Added $functionsAdded type handler functions from '$SchemaFilePath'"
$SchemaFilePath = $null
$oldCount = $functionsToDefine.Count
$foundScriptSchema = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ImportScriptKeywordsFromModule(
$Module, $resource, [ref] $SchemaFilePath, $functionsToDefine )
$functionsAdded = $functionsToDefine.Count - $oldCount
Write-Debug -Message " $Name : PROCESSING RESOURCE FILE: Added $functionsAdded type handler functions from '$SchemaFilePath'"
if ($foundScriptSchema -and $SchemaFilePath)
{
$resourceDirectory = Split-Path $SchemaFilePath
if($resourceDirectory -ne $null)
{
Import-Module -Force: (Test-ModuleReloadRequired $SchemaFilePath) -Verbose:$false -Name $resourceDirectory -Global -ErrorAction SilentlyContinue
}
}
return $foundCimSchema -or $foundScriptSchema
}
#
# A function to write the MOF instance texts of a node to files as meta config
#
function Write-MetaConfigFile
{
param(
[string]
$ConfigurationName,
[string]
$mofNode,
[System.Collections.Generic.Dictionary[string,string]]
$mofNodeHash
)
# Set up prefix for both the configuration and metaconfiguration documents.
$nodeDoc = "/*`n@TargetNode='$mofNode'`n" + "@GeneratedBy=$([system.environment]::UserName)`n@GenerationDate=$(Get-Date)`n@GenerationHost=$([system.environment]::MachineName)`n*/`n"
$nodeConfigurationDocument = $null
[int]$nodeDocCount = 0
$resourceManagers = $null
$resourceManagersCount = 0
$reportManagers = $null
$reportManagersCount = 0
$downloadManagers = $null
$downloadManagersCount = 0
$localConfigManager = $null
$partialConfiguratons = $null
$partialConfigurationCount = 0
$signatureValidationPolicies = $null
$signatureValidationCount = 0
foreach($mofTypeName in $mofNodeHash.Keys)
{
if($mofTypeName -match 'OMI_ConfigurationDocument')
{
$nodeConfigurationDocument = $mofNodeHash[$mofTypeName]
}
if($mofTypeName -match 'MSFT_WebDownloadManager' -or $mofTypeName -match 'MSFT_FileDownloadManager')
{
$r = $mofNodeHash[$mofTypeName] -match '\$\w*'
if($r)
{
if($downloadManagersCount++ -gt 0)
{
$downloadManagers += ",`n"
}
$downloadManagers += ' ' + $Matches[0]
}
$script:SpecifiedV2DownloadManager = $true
}
if($mofTypeName -match 'MSFT_WebResourceManager' -or $mofTypeName -match 'MSFT_FileResourceManager')
{
$r = $mofNodeHash[$mofTypeName] -match '\$\w*'
if($r)
{
if($resourceManagersCount++ -gt 0)
{
$resourceManagers += ",`n"
}
$resourceManagers += ' ' +$Matches[0]
}
}
if($mofTypeName -match 'MSFT_OaaSReportManager' -or $mofTypeName -match 'MSFT_WebReportManager')
{
$r = $mofNodeHash[$mofTypeName] -match '\$\w*'
if($r)
{
if($reportManagersCount++ -gt 0)
{
$reportManagers += ",`n"
}
$reportManagers += ' ' +$Matches[0]
}
}
#MSFT_PartialConfiguration
if($mofTypeName -match 'MSFT_PartialConfiguration')
{
$r = $mofNodeHash[$mofTypeName] -match '\$\w*'
if($r)
{
if($partialConfigurationCount++ -gt 0)
{
$partialConfiguratons += ",`n"
}
$partialConfiguratons += ' ' +$Matches[0]
}
}
#MSFT_SignatureValidation
if($mofTypeName -match 'MSFT_SignatureValidation')
{
$r = $mofNodeHash[$mofTypeName] -match '\$\w*'
if($r)
{
if($signatureValidationCount++ -gt 0)
{
$signatureValidations += ",`n"
}
$signatureValidations += ' ' +$Matches[0]
}
}
if($mofTypeName -notmatch 'MSFT_DSCMetaConfiguration')
{
$nodeDoc += $mofNodeHash[$mofTypeName]
}
else
{
if($localConfigManager -eq $null)
{
# save the localConfigManager which need to be fixed up to add additional manager info as embedded resources
$localConfigManager = $mofNodeHash[$mofTypeName] -replace 'MSFT_DSCMetaConfigurationV2', 'MSFT_DSCMetaConfiguration'
}
else
{
$errorMessage = $LocalizedData.MetaConfigurationHasMoreThanOneLocalConfigurationManager -f @($mofNode)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId InvalidMOFDefinition
Update-ConfigurationErrorCount
return
}
}
$nodeDocCount++
}
if ($script:MetaConfigSetToPull -and (-not $script:SpecifiedV2DownloadManager))
{
$errorMessage = $LocalizedData.PullModeWithoutConfigurationRepository
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId PullModeWithoutConfigurationRepository
Update-ConfigurationErrorCount
}
if ($script:SpecifiedV2DownloadManager -and (-not $script:MetaConfigSetToPull))
{
Write-Warning -Message $LocalizedData.ConfigurationRepositoryWithoutPullMode
}
if($localConfigManager -eq $null)
{
# Print verbose message that empty settings definition is added.
$emptySettingVerboseMessage = $LocalizedData.MetaConfigurationSettingsMissing -f @($mofNode)
Write-Verbose -Message $emptySettingVerboseMessage
# Assign default settings
$localConfigManager = "`ninstance of MSFT_DSCMetaConfiguration as `$MSFT_DSCMetaConfiguration1ref `n{`n};"
}
# fixup to add embedded instances
$nodeDoc += Update-LocalConfigManager $localConfigManager $resourceManagers $reportManagers $downloadManagers $partialConfiguratons $signatureValidations
$nodeOutfile = "$ConfigurationOutputDirectory/$($mofNode).meta.mof"
# add/update OMI_ConfigurationDocument of meta config
if ($nodeDoc -notmatch 'OMI_ConfigurationDocument')
{
if (Get-PSDefaultConfigurationDocument)
{
Write-Debug -Message " ${ConfigurationName}: Adding OMI_ConfigurationDocument from $(Get-PSDefaultConfigurationDocument)"
$nodeDoc += Get-PSDefaultConfigurationDocument
}
else
{
Write-Debug -Message " ${ConfigurationName}: Adding missing OMI_ConfigurationDocument element to the document"
if($Script:NodesPasswordEncrypted[$mofNode])
{
$nodeDoc += "`ninstance of OMI_ConfigurationDocument`n{`n Version=`"2.0.0`";`n MinimumCompatibleVersion = `"$($script:PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'])`";`n CompatibleVersionAdditionalProperties= $(Get-CompatibleVersionAddtionaPropertiesStr);`n Author=`"$([system.environment]::UserName)`";`n GenerationDate=`"$(Get-Date)`";`n GenerationHost=`"$([system.environment]::MachineName)`";`n ContentType=`"PasswordEncrypted`";`n Name=`"$(Get-PSTopConfigurationName)`";`n};"
}
else
{
$nodeDoc += "`ninstance of OMI_ConfigurationDocument`n{`n Version=`"2.0.0`";`n MinimumCompatibleVersion = `"$($script:PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'])`";`n CompatibleVersionAdditionalProperties= $(Get-CompatibleVersionAddtionaPropertiesStr);`n Author=`"$([system.environment]::UserName)`";`n GenerationDate=`"$(Get-Date)`";`n GenerationHost=`"$([system.environment]::MachineName)`";`n Name=`"$(Get-PSTopConfigurationName)`";`n};"
}
}
}
# Fix up newlines to be CRLF
$nodeDoc = $nodeDoc -replace "`n", "`r`n"
# todo: meta configuration might not be verifiable currently
$errMsg = Test-MofInstanceText $nodeDoc
if($errMsg)
{
$errorMessage = $LocalizedData.InvalidMOFDefinition -f @($mofNode, $errMsg)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId InvalidMOFDefinition
Update-ConfigurationErrorCount
$nodeOutfile = "$ConfigurationOutputDirectory/$($mofNode).meta.mof.error"
}
if($nodeDocCount -gt 0)
{
# Write to a file only if no error was generated or we are writing to .mof.error file
if ($Script:PSConfigurationErrors -eq 0 -or $nodeOutfile.EndsWith('mof.error'))
{
$nodeDoc > $nodeOutfile
Get-ChildItem $nodeOutfile
}
}
}
# fixup localConfigmanager to have embedded instance
function Update-LocalConfigManager
{
param(
[string]
$localConfigManager,
[string]
$resourceManagers,
[string]
$reportManagers,
[string]
$downloadManagers,
[string]
$partialConfigurations,
[string]
$signatureValidations
)
$curlyPostion = $localConfigManager.LastIndexOf('}')
if($curlyPostion -gt 0)
{
$first = $localConfigManager.Substring(0, $curlyPostion)
if($resourceManagers)
{
$first += " ResourceModuleManagers = {`n" + $resourceManagers + " `n };`n"
}
if($reportManagers)
{
$first += " ReportManagers = {`n" + $reportManagers + " `n };`n"
}
if($downloadManagers)
{
$first += " ConfigurationDownloadManagers = {`n" + $downloadManagers + " `n };`n"
}
#PartialConfigurations
if($partialConfigurations)
{
$first += " PartialConfigurations = {`n" + $partialConfigurations + " `n };`n"
}
if($signatureValidations)
{
$first += " SignatureValidations = {`n" + $signatureValidations + " `n };`n"
}
$first += "};`n"
$first
}
}
function Get-MofInstanceName
{
param(
[string]
$mofInstance
)
}
#
# A function to write the MOF instance texts of a node to files
#
function Write-NodeMOFFile
{
param(
[string]
$ConfigurationName,
[string]
$mofNode,
[System.Collections.Generic.Dictionary[string,string]]
$mofNodeHash
)
if ($script:MetaConfigSetToPull -and (-not $script:SpecifiedV1DownloadManagerName))
{
$errorMessage = $LocalizedData.PullModeWithoutDownloadManager
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId PullModeWithoutDownloadManager
Update-ConfigurationErrorCount
}
if ($script:SpecifiedV1DownloadManagerName -and (-not $script:MetaConfigSetToPull))
{
Write-Warning -Message $LocalizedData.DownloadManagerWithoutPullMode
}
# Set up prefix for both the configuration and metaconfiguration documents.
$nodeDoc = "/*`n@TargetNode='$mofNode'`n" + "@GeneratedBy=$([system.environment]::UserName)`n@GenerationDate=$(Get-Date)`n@GenerationHost=$([system.environment]::MachineName)`n*/`n"
$nodeMetaDoc = $nodeDoc
$nodeConfigurationDocument = $null
[int]$metaDocCount = 0
[int]$nodeDocCount = 0
foreach($mofTypeName in $mofNodeHash.Keys)
{
if($mofTypeName -match 'MSFT_DSCMetaConfiguration')
{
$tempMetaDoc = $mofNodeHash[$mofTypeName]
$metaDocCount++
break
}
}
foreach($mofTypeName in $mofNodeHash.Keys)
{
if(($mofTypeName -notmatch 'MSFT_DSCMetaConfiguration'))
{
if(($metaDocCount -gt 0) -and ($tempMetaDoc -match [regex]::Escape($mofTypeName)))
{
$nodeMetaDoc += $mofNodeHash[$mofTypeName]
}
else
{
if($mofTypeName -match 'OMI_ConfigurationDocument')
{
$nodeConfigurationDocument = $mofNodeHash[$mofTypeName]
}
$nodeDoc += $mofNodeHash[$mofTypeName]
$nodeDocCount++
}
}
}
$nodeMetaDoc += $tempMetaDoc
$nodeOutfile = "$ConfigurationOutputDirectory/$($mofNode).mof"
if($metaDocCount -gt 0)
{
$nodeMetaOutfile = "$ConfigurationOutputDirectory/$($mofNode).meta.mof"
# this meta config uses v1 schema so will not have v2 properties
if ($nodeConfigurationDocument)
{
Write-Debug -Message " ${ConfigurationName}: Adding OMI_ConfigurationDocument from the current node '$mofNode': $nodeConfigurationDocument"
$nodeMetaDoc += $nodeConfigurationDocument
}
elseif (Get-PSDefaultConfigurationDocument)
{
Write-Debug -Message " ${ConfigurationName}: Adding OMI_ConfigurationDocument from $(Get-PSDefaultConfigurationDocument)"
$nodeMetaDoc += Get-PSDefaultConfigurationDocument
}
else
{
Write-Debug -Message " ${ConfigurationName}: Adding missing OMI_ConfigurationDocument element to the document"
if($Script:NodesPasswordEncrypted[$mofNode])
{
$nodeMetaDoc += "`ninstance of OMI_ConfigurationDocument`n{`n Version=`"2.0.0`";`n MinimumCompatibleVersion = `"1.0.0`";`n CompatibleVersionAdditionalProperties= $(Get-CompatibleVersionAddtionaPropertiesStr);`n Author=`"$([system.environment]::UserName)`";`n GenerationDate=`"$(Get-Date)`";`n GenerationHost=`"$([system.environment]::MachineName)`";`n ContentType=`"PasswordEncrypted`";`n Name=`"$(Get-PSTopConfigurationName)`";`n};"
}
else
{
$nodeMetaDoc += "`ninstance of OMI_ConfigurationDocument`n{`n Version=`"2.0.0`";`n MinimumCompatibleVersion = `"1.0.0`";`n CompatibleVersionAdditionalProperties= $(Get-CompatibleVersionAddtionaPropertiesStr);`n Author=`"$([system.environment]::UserName)`";`n GenerationDate=`"$(Get-Date)`";`n GenerationHost=`"$([system.environment]::MachineName)`";`n Name=`"$(Get-PSTopConfigurationName)`";`n};"
}
}
}
if ($nodeDoc -notmatch 'OMI_ConfigurationDocument')
{
if (Get-PSDefaultConfigurationDocument)
{
Write-Debug -Message " ${ConfigurationName}: Adding OMI_ConfigurationDocument from $(Get-PSDefaultConfigurationDocument)"
$nodeDoc += Get-PSDefaultConfigurationDocument
}
else
{
Write-Debug -Message " ${ConfigurationName}: Adding missing OMI_ConfigurationDocument element to the document"
if($Script:NodesPasswordEncrypted[$mofNode])
{
if($nodeDoc.Contains("PsDscRunAsCredential"))
{
$nodeDoc += "`ninstance of OMI_ConfigurationDocument`n
{`n Version=`"2.0.0`";`n
MinimumCompatibleVersion = `"2.0.0`";`n
CompatibleVersionAdditionalProperties= {`"Omi_BaseResource:ConfigurationName`"};`n
Author=`"$([system.environment]::UserName)`";`n
GenerationDate=`"$(Get-Date)`";`n
GenerationHost=`"$([system.environment]::MachineName)`";`n
ContentType=`"PasswordEncrypted`";`n
Name=`"$(Get-PSTopConfigurationName)`";`n
};"
}
else
{
$nodeDoc += "`ninstance of OMI_ConfigurationDocument`n
{`n Version=`"2.0.0`";`n
MinimumCompatibleVersion = `"1.0.0`";`n
CompatibleVersionAdditionalProperties= {`"Omi_BaseResource:ConfigurationName`"};`n
Author=`"$([system.environment]::UserName)`";`n
GenerationDate=`"$(Get-Date)`";`n
GenerationHost=`"$([system.environment]::MachineName)`";`n
ContentType=`"PasswordEncrypted`";`n
Name=`"$(Get-PSTopConfigurationName)`";`n
};"
}
}
else
{
if($nodeDoc.Contains("PsDscRunAsCredential"))
{
$nodeDoc += "`ninstance of OMI_ConfigurationDocument`n
{`n Version=`"2.0.0`";`n
MinimumCompatibleVersion = `"2.0.0`";`n
CompatibleVersionAdditionalProperties= {`"Omi_BaseResource:ConfigurationName`"};`n
Author=`"$([system.environment]::UserName)`";`n
GenerationDate=`"$(Get-Date)`";`n
GenerationHost=`"$([system.environment]::MachineName)`";`n
Name=`"$(Get-PSTopConfigurationName)`";`n
};"
}
else
{
$nodeDoc += "`ninstance of OMI_ConfigurationDocument`n
{`n Version=`"2.0.0`";`n
MinimumCompatibleVersion = `"1.0.0`";`n
CompatibleVersionAdditionalProperties= {`"Omi_BaseResource:ConfigurationName`"};`n
Author=`"$([system.environment]::UserName)`";`n
GenerationDate=`"$(Get-Date)`";`n
GenerationHost=`"$([system.environment]::MachineName)`";`n
Name=`"$(Get-PSTopConfigurationName)`";`n
};"
}
}
}
}
# Fix up newlines to be CRLF
$nodeDoc = $nodeDoc -replace "`n", "`r`n"
$errMsg = Test-MofInstanceText $nodeDoc
if($errMsg)
{
$errorMessage = $LocalizedData.InvalidMOFDefinition -f @($mofNode, $errMsg)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId InvalidMOFDefinition
Update-ConfigurationErrorCount
$nodeOutfile = "$ConfigurationOutputDirectory/$($mofNode).mof.error"
}
if($nodeDocCount -gt 0)
{
# Write to a file only if no error was generated or we are writing to .mof.error file
if ($Script:PSConfigurationErrors -eq 0 -or $nodeOutfile.EndsWith('mof.error'))
{
$nodeDoc > $nodeOutfile
Get-ChildItem $nodeOutfile
}
}
if($nodeMetaDoc -match 'MSFT_DSCMetaConfiguration' -and $Script:PSConfigurationErrors -eq 0)
{
$nodeMetaDoc = $nodeMetaDoc -replace "`n", "`r`n"
$nodeMetaDoc > $nodeMetaOutfile
Get-ChildItem $nodeMetaOutfile
}
}
#
# A function to make sure that only valid resources are referenced within a node. It
# operates off of the $Script:NodeResources dictionary. An empty dictionary is not
# considered an error since this function is called at both the node level and the configuration
# level.
# This function also expand the dependsOn to composite resources
#
function ValidateNodeResources
{
Write-Debug -Message " Validating resource set for node: $(Get-PSCurrentConfigurationNode)"
$newNodeResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,[string[]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
if ($Script:NodeResources)
{
foreach ($resourceId in $Script:NodeResources.Keys)
{
Write-Debug -Message " Checking node $resourceId"
$newDependsOn = foreach ($requiredResource in $Script:NodeResources[$resourceId])
{
# Skip resources that have no DependsOn.
if ($requiredResource)
{
Write-Debug -Message " > Checking for required node $requiredResource"
if (-not $Script:NodeResources.ContainsKey($requiredResource))
{
Write-Debug -Message " > trying expand for required node $requiredResource"
$expandedDependsOn = foreach($rId in $Script:NodeResources.keys)
{
if($rId.EndsWith($requiredResource, [System.StringComparison]::InvariantCultureIgnoreCase))
{
$rId
}
}
if(-not $expandedDependsOn)
{
$errorMessage = $LocalizedData.RequiredResourceNotFound -f @($requiredResource, $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId RequiredResourceNotFound
Update-ConfigurationErrorCount
}
else
{
$expandedDependsOn
}
}
else
{
$requiredResource
}
}
}
if($newDependsOn)
{
$newNodeResources[$resourceId] = $newDependsOn
}
}
if($newNodeResources)
{
foreach($id in $newNodeResources.keys)
{
$Script:NodeResources[$id] = $newNodeResources[$id]
}
}
}
Write-Debug -Message " Validation complete for node: $(Get-PSCurrentConfigurationNode)"
}
#
# A function to make sure that only valid resources are referenced within the resource without node.
# This function also expand the dependsOn to composite resources
#
function ValidateNoNameNodeResources
{
Write-Debug -Message ' Validating resource set for resources in the default configuration'
$newNodeResources = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,[string[]]]' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
if ($Script:NoNameNodesResources)
{
foreach ($resourceId in $Script:NoNameNodesResources.Keys)
{
Write-Debug -Message " Checking node $resourceId"
$newDependsOn = foreach ($requiredResource in $Script:NoNameNodesResources[$resourceId])
{
# Skip resources that have no DependsOn.
if ($requiredResource)
{
Write-Debug -Message " > Checking for required node $requiredResource"
if (-not $Script:NoNameNodesResources.ContainsKey($requiredResource))
{
Write-Debug -Message " > trying expand for required node $requiredResource"
$expandedDependsOn = foreach($rId in $Script:NoNameNodesResources.keys)
{
if($rId.EndsWith($requiredResource, [System.StringComparison]::InvariantCultureIgnoreCase))
{
$rId
}
}
if(-not $expandedDependsOn)
{
# When Node name is not specified in the configuration, $NoNameNodesResources & $NodeResources list are same.
# Do not throw error here to avoid duplicate errors, these errors will be throw by ‘ValidateNodeResources’ when $NodeResources list will be validated.
if(-not $Script:NodeResources.ContainsKey($resourceId))
{
$errorMessage = $LocalizedData.RequiredResourceNotFound -f @($requiredResource, $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId RequiredResourceNotFound
Update-ConfigurationErrorCount
}
}
else
{
$expandedDependsOn
}
}
else
{
$requiredResource
}
}
}
if($newDependsOn)
{
$newNodeResources[$resourceId] = $newDependsOn
}
}
if($newNodeResources)
{
foreach($id in $newNodeResources.keys)
{
$Script:NoNameNodesResources[$id] = $newNodeResources[$id]
}
}
}
Write-Debug -Message ' Validation complete for default resources.'
}
#
# A function to make sure that only valid Manager are referenced within a node. It
# operates off of the $Script:NodeManager dictionary.
# An empty dictionary is not
# considered an error since this function is called at both the node level and the configuration
# level.
#
function ValidateNodeManager
{
Write-Debug -Message " Validating manager set for node: $(Get-PSCurrentConfigurationNode)"
if ($Script:NodeManager)
{
foreach ($resourceId in $Script:NodeManager.Keys)
{
Write-Debug -Message " Checking node $resourceId"
$refManagers = $Script:NodeManager[$resourceId]
# Skip partial configuration that have no Manager.
if ($refManagers)
{
Write-Debug -Message " > Checking for required manager $refManagers"
foreach ($refManager in $refManagers)
{
if (-not $Script:NodeResources.ContainsKey($refManager))
{
$errorMessage = $LocalizedData.ReferencedManagerNotFound -f @($refManager, $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ReferencedManagerNotFound
Update-ConfigurationErrorCount
}
}
}
}
}
if ($IsMetaConfig)
{
#region Reference check for SignatureValidationPolicy
$referencedSignaturePolicy = $null;
$nodeAliases = @{}
if ($NoNameNodeInstanceAliases.Count -ne 0)
{
$nodeAliases = $NoNameNodeInstanceAliases
}
else
{
$nodeAliases = $NodeInstanceAliases[$Node]
}
$metaConfigReferenceId = $nodeAliases.Keys | Where-Object -FilterScript {$_ -like '$MSFT_DSCMetaConfiguration*'}
# If there is no setting block defined.
if(!$metaConfigReferenceId) {return}
$v2MetaConfigReference = $nodeAliases.Item($metaConfigReferenceId)
# Check if user specified the signature policy in the setting block.
$signatrueValidationIsSpecified = $v2MetaConfigReference -match 'SignatureValidationPolicy[ ]*='
if ($signatrueValidationIsSpecified)
{
$referencedSignaturePolicyMatches = $v2MetaConfigReference -match '\[SignatureValidation\][^"]+'
if ($referencedSignaturePolicyMatches)
{
$referencedSignaturePolicy = $Matches[0]
}
else
{
$errorMessage = $LocalizedData.IncorrectSignatureValidationPolicyFormat
$exception = New-Object -TypeName System.ArgumentException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidArgument -ErrorId IncorrectSignatureValidationPolicyFormat
Update-ConfigurationErrorCount
}
# If there is a referenced policy ensure that its value matches one of the defined signature validation resources.
if ($referencedSignaturePolicy)
{
if (-not $Script:NodeResources.ContainsKey($referencedSignaturePolicy))
{
$errorMessage = $LocalizedData.ReferencedPolicyNotDefined -f @($referencedSignaturePolicy)
$exception = New-Object -TypeName System.ArgumentException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidArgument -ErrorId ReferencedPolicyNotDefined
Update-ConfigurationErrorCount
}
}
}
}
#endregion
Write-Debug -Message " Validation complete for node: $(Get-PSCurrentConfigurationNode)"
}
#
# A function to make sure that only valid resource source are referenced within a node. It
# operates off of the $Script:NodeResourceSource dictionary.
# An empty dictionary is not
# considered an error since this function is called at both the node level and the configuration
# level.
#
function ValidateNodeResourceSource
{
Write-Debug -Message " Validating resource source set for node: $(Get-PSCurrentConfigurationNode)"
if ($Script:NodeResourceSource)
{
foreach ($resourceId in $Script:NodeResourceSource.Keys)
{
Write-Debug -Message " Checking node $resourceId"
$refResourceSources = $Script:NodeResourceSource[$resourceId]
# Skip partial configuration that have no Manager.
if ($refResourceSources)
{
Write-Debug -Message " > Checking for required manager $refManagers"
foreach ($refResourceSource in $refResourceSources)
{
if (-not $Script:NodeResources.ContainsKey($refResourceSource))
{
$errorMessage = $LocalizedData.ReferencedResourceSourceNotFound -f @($refResourceSource, $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ReferencedResourceSourceNotFound
Update-ConfigurationErrorCount
}
}
}
}
}
Write-Debug -Message " Validation complete for node: $(Get-PSCurrentConfigurationNode)"
}
#
# A function to make sure that exclusive resources among partial configurations do not conflict with each other
# it is already of format modulename\resourcName or modulename\* or resourceName
# It also validate they exist
function ValidateNodeExclusiveResources
{
Write-Debug -Message " Validating exclusive resources for node: $(Get-PSCurrentConfigurationNode)"
if ($Script:NodeExclusiveResources)
{
# map will be of format {moduleName, {Id=ResourceId; ContainsAll = $true/$false; Resources=@('r1','r2'...)}}
$ModuleBasedExclusiveResourceMap = @{}
# map for resource that doesn't have module
$NoModuleExclusiveResourceMap = @{}
foreach ($resourceId in $Script:NodeExclusiveResources.Keys)
{
Write-Debug -Message " Checking node $resourceId"
# Remove duplicate entries from exclusive Resource array
$exclusiveResourceList = $Script:NodeExclusiveResources[$resourceId] | select -uniq
foreach($refResource in $exclusiveResourceList)
{
$resourceSegs = $refResource -split '\\'
if($resourceSegs.Length -eq 2)
{
if($ModuleBasedExclusiveResourceMap[$resourceSegs[0]] -eq $null)
{
$ModuleBasedExclusiveResourceMap[$resourceSegs[0]] = @{
Id = $resourceId
}
if($resourceSegs[1] -eq '*')
{
$ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['ContainsAll'] = $true
}
else
{
$ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['ContainsAll'] = $false
$ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['Resources'] = @($resourceSegs[1])
}
}
else
{
# 'Module\Resource' in PartialConfiguration1 conflicts with 'Module\*' in PartialConfiguration2
# or 'Module\*' in PartialConfiguration1 conflicts with 'Module\Resource' in PartialConfiguration2
if(($resourceSegs[1] -eq '*') -or $ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['ContainsAll'])
{
$errorMessage = $LocalizedData.ConflictInExclusiveResources -f @($ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['Id'], $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConflictInExclusiveResources
Update-ConfigurationErrorCount
break
}
# 'Module\Resource' in PartialConfiguration1 conflicts with 'Module\Resource' in PartialConfiguration2
# or 'Module\Resource' in PartialConfiguration1 conflicts with 'Resource' in PartialConfiguration2
elseif($ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['Resources'] -icontains $resourceSegs[1] `
-or $NoModuleExclusiveResourceMap[$resourceSegs[1]] -ne $null)
{
$errorMessage = $LocalizedData.ConflictInExclusiveResources -f @($ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['Id'], $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConflictInExclusiveResources
Update-ConfigurationErrorCount
break
}
else
{
$ModuleBasedExclusiveResourceMap[$resourceSegs[0]]['Resources'] += @($resourceSegs[1])
}
}
}
else # no module name, normally means binary resource
{
if($NoModuleExclusiveResourceMap[$refResource] -eq $null)
{
$resourceFound = $false
$ModuleBasedExclusiveResourceMap.GetEnumerator() | % {
if($_.Value.Resources -icontains $refResource) {
$resourceFound = $true
$ConflictingPartialConfigurationId = $_.Value.Id
}
}
# 'Resource' in PartialConfiguration1 conflicts with 'Module\Resource' in PartialConfiguration2
if($resourceFound){
$errorMessage = $LocalizedData.ConflictInExclusiveResources -f @($ConflictingPartialConfigurationId, $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConflictInExclusiveResources
Update-ConfigurationErrorCount
break
}
$NoModuleExclusiveResourceMap[$refResource] = @{
Id = $resourceId
}
}
# 'Resource' in PartialConfiguration1 conflicts with 'Resource' in PartialConfiguration2
else
{
$errorMessage = $LocalizedData.ConflictInExclusiveResources -f @($NoModuleExclusiveResourceMap[$refResource]['Id'], $resourceId)
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId ConflictInExclusiveResources
Update-ConfigurationErrorCount
break
}
}
}
}
}
Write-Debug -Message " Validation complete for node: $(Get-PSCurrentConfigurationNode)"
}
#
# A function to make sure that only valid resources are referenced within a node. It
# operates off of the $Script:NodeResources dictionary. An empty dictionary is not
# considered an error since this function is called at both the node level and the configuration
# level.
# it uses Tarjan strongly connected component algorithms
#
function ValidateNoCircleInNodeResources
{
Write-Debug -Message " Validating resource set for node: $(Get-PSCurrentConfigurationNode)"
[int] $script:CircleIndex = 0
[System.Collections.Generic.Stack[string]] $script:resourceIdStack =
New-Object -TypeName 'System.Collections.Generic.Stack[string]'
[hashtable] $script:resourceIndex = @{}
[hashtable] $script:resourceLowIndex = @{}
[int] $script:ComponentDepth = 0
[int] $script:MaxComponentDepth = 1024
if ($Script:NodeResources)
{
foreach ($resourceId in $Script:NodeResources.Keys)
{
if(($Script:NodeResources[$resourceId] -ne $null) -and $Script:NodeResources[$resourceId].Contains($resourceId))
{
$errorMessage = $LocalizedData.DependsOnLoopDetected -f "$resourceId->$resourceId"
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId DependsOnLoopDetected
Update-ConfigurationErrorCount
}
if($resourceIndex[$resourceId] -eq $null)
{
$script:ComponentDepth = 0
StrongConnect($resourceId)
}
}
}
Write-Debug -Message " Validation circular reference completed for node: $(Get-PSCurrentConfigurationNode)"
}
function StrongConnect
{
param ([string]$resourceId)
$script:resourceIndex[$resourceId] = $script:CircleIndex
$script:resourceLowIndex[$resourceId] = $script:CircleIndex
$script:CircleIndex++
$script:ComponentDepth++
if($script:ComponentDepth -gt $script:MaxComponentDepth)
{
$errorMessage = $LocalizedData.DependsOnLinkTooDeep -f $script:MaxComponentDepth
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId DependsOnLinkTooDeep
Update-ConfigurationErrorCount
}
$script:resourceIdStack.Push($resourceId)
foreach ($requiredResource in $Script:NodeResources[$resourceId])
{
Write-Debug -Message " > Checking for required node $requiredResource"
#$requiredResource is not visited yet
if(($requiredResource -ne $null) -and ($script:resourceIndex[$requiredResource] -eq $null))
{
StrongConnect($requiredResource)
$script:resourceLowIndex[$resourceId] = [math]::Min($script:resourceLowIndex[$resourceId], $script:resourceLowIndex[$requiredResource])
}
elseif($script:resourceIdStack -Contains $requiredResource)
{
$script:resourceLowIndex[$resourceId] = [math]::Min($script:resourceLowIndex[$resourceId], $script:resourceIndex[$requiredResource])
}
}
if($script:resourceIndex[$resourceId] -eq $script:resourceLowIndex[$resourceId])
{
$resourceCount = 0
$circularLinks = ''
do
{
$a = $script:resourceIdStack.Pop()
$circularLinks += "->$a"
$resourceCount++
}
while($a -ne $resourceId)
if($resourceCount -gt 1)
{
$errorMessage = $LocalizedData.DependsOnLoopDetected -f $circularLinks
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId DependsOnLoopDetected
Update-ConfigurationErrorCount
}
}
}
#
# Returns any validation error messages
#
function Test-MofInstanceText
{
param (
[Parameter(Mandatory)]
$instanceText
)
# Ignore empty instances...
if ( $instanceText)
{
try
{
[Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ValidateInstanceText($instanceText)
}
catch [System.Management.Automation.MethodInvocationException]
{
# Return the exception message from the inner most ErrorRecord
$ErrorRecord = Get-InnerMostErrorRecord $_
$ErrorRecord.Exception.Message
}
}
}
#
# Encrypt a password using CMS
#
function Get-EncryptedPassword
{
param (
[Parameter()]
$Value = $null
)
$cert = $null
if($Node -and $selectedNodesData)
{
if($selectedNodesData -is [array])
{
foreach($target in $selectedNodesData)
{
# Node name should be exactly the same as one defined in AllNodes
# -eq does case in sensitive comparison
if($target['NodeName'] -and $target['NodeName'] -eq $Node)
{
$currentNode = $target
}
}
}
else
{
$currentNode = $selectedNodesData
}
}
# where user need to specify properties for resources not in a node,
# they can do it through localhost nodeName in $allNodes
elseif($allnodes -and $allnodes.AllNodes)
{
foreach($target in $allnodes.AllNodes)
{
if($target['NodeName'] -and $target['NodeName'] -eq 'localhost')
{
$currentNode = $target
}
}
}
if($currentNode)
{
# if Certificate is provided, it override PSDscAllowPlainTextPassword : bug 565167
# CertificateID is currently assumed to be the 'thumbprint' from the certificate
# Protect-CmsMessage [-To] takes actual cert, path to cert file, path to a directory contains cert, thumbprint or subject name of cert
# we only support cert file and thumbprint as before now
$certificateid = $currentNode['CertificateID']
# If there is no certificateid defined, just return the original value...
if ( -not $certificateid)
{
# CertificateFile is the public key file
$certificatefile = $currentNode['CertificateFile']
if ( -not $certificatefile)
{
return $Value
}
else
{
$CmsMessageRecipient = $certificatefile
}
}
else
{
$CmsMessageRecipient = $certificateid
}
}
if($CmsMessageRecipient -and $Value -is [string])
{
# Encrypt using the public key
$encMsg =Protect-CmsMessage -To $CmsMessageRecipient -Content $Value
# Reverse bytes for unmanaged decryption
#[Array]::Reverse($encbytes)
#$encMsg = $encMsg -replace '-----BEGIN CMS-----',''
#$encMsg = $encMsg -replace "`n",''
#$encMsg = $encMsg -replace '-----END CMS-----',''
return $encMsg
}
else
{
# passwords should be some type of string so this is probably an error but pass
# back the incoming value. Also if there is no key, then we just pass through the
# password as is.
$Value
}
}
#
# Retrieve a public key that can be used for encryption. Matching on thumbprint
#
function Get-PublicKeyFromStore
{
param(
[Parameter(Mandatory)]
[string]
$certificateid
)
$cert = $null
foreach($certIndex in Get-ChildItem -Path cert:\LocalMachine\My)
{
if($certIndex.Thumbprint -match $certificateid)
{
$cert = $certIndex
break
}
}
if(-not $cert)
{
$errorMessage = $($LocalizedData.CertificateStoreReadError) -f $certificateid
ThrowError -ExceptionName 'System.InvalidOperationException' -ExceptionMessage $errorMessage -ExceptionObject $certificateid -ErrorId 'InvalidPathSpecified' -ErrorCategory InvalidOperation
}
else
{
$cert
}
}
#
# Retrieve a public key that can be used for encryption. Certificate loaded from
# a certificate file
#
function Get-PublicKeyFromFile
{
param(
[Parameter(Mandatory)]
[string]
$certificatefile
)
try
{
$cert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2
if($cert)
{
$cert.Import($certificatefile)
$cert
}
}
catch
{
$errorMessage = $($LocalizedData.CertificateFileReadError) -f $certificatefile
ThrowError -ExceptionName 'System.InvalidOperationException' -ExceptionMessage $errorMessage -ExceptionObject $certificatefile -ErrorId 'InvalidPathSpecified' -ErrorCategory InvalidOperation
}
}
###########################################################
#
# Checksum generation functions.
#
###########################################################
#-----------------------------------------------------------------------------------------------------
# New-DscChecksum cmdlet is used to create corresponding checksum files for a specified file or folder
#-----------------------------------------------------------------------------------------------------
function New-DscChecksum
{
[CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'http://go.microsoft.com/fwlink/?LinkId=403986')]
param(
[Parameter(Mandatory)]
[Alias('ConfigurationPath')]
[ValidateNotNullOrEmpty()]
[string[]]
$Path,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string]
$OutPath = $null,
[switch]
$Force
)
# Check validity of all configuration paths specified, throw if any of them is invalid
for ($i = 0 ; $i -lt $Path.Length ; $i++)
{
if (!(Test-Path -Path $Path[$i]))
{
$errorMessage = $LocalizedData.InvalidConfigPath -f $Path[$i]
ThrowError -ExceptionName 'System.ArgumentException' -ExceptionMessage $errorMessage -ExceptionObject $Path[$i] -ErrorId 'InvalidConfigurationPath' -ErrorCategory InvalidArgument
}
}
# If an OutPath is specified, handle its creation and error conditions
if ($OutPath)
{
# If and invalid path syntax is specified in $Outpath, throw
if(([System.IO.Path]::GetInvalidPathChars() | ForEach-Object -Process {
$OutPath.Contains($_)
}
).IndexOf($true)[0] -ne -1)
{
$errorMessage = $LocalizedData.InvalidOutpath -f $OutPath
ThrowError -ExceptionName 'System.ArgumentException' -ExceptionMessage $errorMessage -ExceptionObject $OutPath -ErrorId 'InvalidOutPath' -ErrorCategory InvalidArgument
}
# If the specified $Outpath conflicts with an existing file, throw
if(Test-Path -Path $OutPath -PathType Leaf)
{
$errorMessage = $LocalizedData.OutpathConflict -f $OutPath
ThrowError -ExceptionName 'System.ArgumentException' -ExceptionMessage $errorMessage -ExceptionObject $OutPath -ErrorId 'InvalidOutPath' -ErrorCategory InvalidArgument
}
# IF THE CONTROL REACHED HERE, $OutPath IS A VALID DIRECTORY PATH WHICH HAS NO CONFLICT WITH AN EXISTING FILE
# If $OutPath doesn't exist, create it
if(!(Test-Path -Path $OutPath))
{
$null = New-Item -Path $OutPath -ItemType Directory
}
$OutPath = (Resolve-Path $OutPath).ProviderPath
}
# Retrieve all valid configuration files at the specified $Path
$allConfigFiles = $Path | ForEach-Object -Process {
(Get-ChildItem -Path $_ -Recurse | Where-Object -FilterScript {
$_.Extension -eq '.mof' -or $_.Extension -eq '.zip'
}
)
}
# If no valid config file was found, log this and return
if ($allConfigFiles.Length -eq 0)
{
Write-Log -Message $LocalizedData.NoValidConfigFileFound
return
}
# IF THE CONTROL REACHED HERE, VALID CONFIGURATION FILES HAVE BEEN FOUND AND WE NEED TO CALCULATE THEIR HASHES
foreach ($file in $allConfigFiles)
{
$fileOutpath = "$($file.FullName).checksum"
if ($OutPath)
{
$fileOutpath = "$OutPath\$($file.Name).checksum"
}
# If the Force parameter was not specified and the hash file already exists for the current file, log this, and skip this file
if (!$Force -and (Get-Item -Path $fileOutpath -ErrorAction SilentlyContinue))
{
Write-Log -Message ($LocalizedData.CheckSumFileExists -f $fileOutpath)
continue
}
# Devise appropriate message
$message = $LocalizedData.CreateChecksumFile -f $fileOutpath
if (Test-Path -Path $fileOutpath)
{
$message = $LocalizedData.OverwriteChecksumFile -f $fileOutpath
}
# Finally, if the hash file doesn't exist already or -Force has been specified, then output the corresponding hash file
if ($pscmdlet.ShouldProcess($message, $null, $null))
{
[String]$checksum = (Get-FileHash -Path $file.FullName -Algorithm SHA256).Hash
WriteFile -Path $fileOutpath -Value $checksum
}
}
}
Export-ModuleMember -Function New-DscChecksum
#------------------------------------
# Utility to throw an error/exception
#------------------------------------
function ThrowError
{
param
(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$ExceptionName,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$ExceptionMessage,
[System.Object]
$ExceptionObject,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$errorId,
[parameter(Mandatory = $true)]
[ValidateNotNull()]
[System.Management.Automation.ErrorCategory]
$errorCategory
)
$exception = New-Object $ExceptionName $ExceptionMessage
$ErrorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, $errorId, $errorCategory, $ExceptionObject
throw $ErrorRecord
}
#----------------------------------------
# Utility to write WhatIf or Verbose logs
#----------------------------------------
function Write-Log
{
[CmdletBinding(SupportsShouldProcess = $true)]
param
(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.String]
$message
)
if ($pscmdlet.ShouldProcess($message, $null, $null))
{
Write-Verbose -Message $message
}
}
# WriteFile is a helper function used to write the content to the file
function WriteFile
{
param(
[parameter(Mandatory)]
[string]
$Value,
[parameter(Mandatory)]
[string]
$Path)
try
{
[system.io.streamwriter]$stream = New-Object -TypeName system.io.StreamWriter -ArgumentList ($Path, $false)
try
{
[void] $stream.Write($Value)
}
finally
{
if ($stream)
{
$stream.Close()
}
}
}
catch
{
$errorMessage = $LocalizedData.FileReadError -f $Path
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId InvalidPathSpecified
Update-ConfigurationErrorCount
}
}
#
# ReadEnvironmentFile imports the contents of a
# file as ConfigurationData
#
function ReadEnvironmentFile
{
param(
[parameter(Mandatory)]
[string]
$FilePath)
try
{
$resolvedPath = Resolve-Path $FilePath
}
catch
{
$errorMessage = $LocalizedData.FilePathError -f $FilePath
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId InvalidPathSpecified
Update-ConfigurationErrorCount
}
try
{
$content = Get-Content $resolvedPath -Raw
$sb = [scriptblock]::Create($content)
$sb.CheckRestrictedLanguage($null, $null, $true)
$sb.Invoke()
}
catch
{
$errorMessage = $LocalizedData.EnvironmentContentError -f $FilePath
$exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $errorMessage
Write-Error -Exception $exception -Message $errorMessage -Category InvalidOperation -ErrorId InvalidEnvironmentContentSpecified
Update-ConfigurationErrorCount
}
}
function Get-DSCResourceModules
{
$listPSModuleFolders = $env:PSModulePath.Split(";")
$dscModuleFolderList = [System.Collections.Generic.HashSet[System.String]]::new()
foreach ($folder in $listPSModuleFolders)
{
$folder = $folder.Trim()
if (!(Test-Path $folder))
{
continue
}
foreach($moduleFolder in Get-ChildItem $folder -Directory)
{
$addModule = $false
$dscFolders = Get-childitem "$($moduleFolder.FullName)\DscResources","$($moduleFolder.FullName)\*\DscResources" -ErrorAction Ignore
if($dscFolders -ne $null)
{
$addModule = $true
}
if(-not $addModule)
{
foreach($psd1 in Get-ChildItem -Recurse -Filter "$($moduleFolder.Name).psd1" -Path $moduleFolder.fullname -Depth 2)
{
$containsDSCResource = select-string -LiteralPath $psd1 -pattern '^(?!#).*\bDscResourcesToExport\b.*'
if($containsDSCResource -ne $null)
{
$addModule = $true
}
}
}
if($addModule)
{
$dscModuleFolderList.Add($moduleFolder.Name)
}
}
}
$dscModuleFolderList
}
###########################################################
# Get-DSCResource
###########################################################
#
# Gets DSC resources on the machine. Allows to filter on a particular resource.
# It parses all the resources defined in the schema.mof file and also the composite
# resources defined or imported from PowerShell modules
#
function Get-DscResource
{
[CmdletBinding(HelpUri = 'http://go.microsoft.com/fwlink/?LinkId=403985')]
[OutputType('Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo[]')]
[OutputType('string[]')]
param (
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string[]]
$Name,
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[Object]
$Module,
[Parameter()]
[switch]
$Syntax
)
Begin
{
$initialized = $false
$ModuleString = $null
Write-Progress -Id 1 -Activity $LocalizedData.LoadingDefaultCimKeywords
$keywordErrors = New-Object -TypeName 'System.Collections.ObjectModel.Collection[System.Exception]'
# Load the default Inbox providers (keyword) in cache, also allow caching the resources from multiple versions of modules.
[Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::LoadDefaultCimKeywords($keywordErrors, $true)
foreach($ex in $keywordErrors)
{
Write-Error -Exception $ex
if($ex.InnerException)
{
Write-Error -Exception $ex.InnerException
}
}
Write-Progress -Id 2 -Activity $LocalizedData.GettingModuleList
$initialized = $true
if($Module) #Pick from the specified module if there's one
{
$moduleSpecificName = [System.Management.Automation.LanguagePrimitives]::ConvertTo($Module,[Microsoft.PowerShell.Commands.ModuleSpecification])
$modules = Get-Module -ListAvailable -FullyQualifiedName $moduleSpecificName
if($Module -is [System.Collections.Hashtable])
{
$ModuleString = $Module.ModuleName
}
else
{
$ModuleString = $Module
}
}
else
{
$modules = Get-Module -ListAvailable -Name (Get-DSCResourceModules)
}
foreach ($mod in $modules)
{
if ($mod.ExportedDscResources.Count -gt 0)
{
$null = ImportClassResourcesFromModule -Module $mod -Resources * -functionsToDefine $functionsToDefine
}
$dscResources = Join-Path -Path $mod.ModuleBase -ChildPath 'DSCResources'
if(Test-Path $dscResources)
{
foreach ($resource in Get-ChildItem -Path $dscResources -Directory -Name)
{
$null = ImportCimAndScriptKeywordsFromModule -Module $mod -Resource $resource -functionsToDefine $functionsToDefine
}
}
}
$Resources = @()
}
Process
{
try
{
if ($Name -ne $null)
{
$nameMessage = $LocalizedData.GetDscResourceInputName -f @('Name', [system.string]::Join(', ', $Name))
Write-Verbose -Message $nameMessage
}
if($Module -and !$modules)
{
#Return if no modules were found with the required specification
Write-Warning -Message $LocalizedData.NoModulesPresent
return
}
$ignoreResourceParameters = @('InstanceName', 'OutputPath', 'ConfigurationData') + [System.Management.Automation.Cmdlet]::CommonParameters + [System.Management.Automation.Cmdlet]::OptionalCommonParameters
$patterns = GetPatterns $Name
Write-Progress -Id 3 -Activity $LocalizedData.CreatingResourceList
# Get resources for CIM cache
$keywords = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::GetCachedKeywords() | Where-Object -FilterScript {
(!$_.IsReservedKeyword) -and ($_.ResourceName -ne $null) -and !(IsHiddenResource $_.ResourceName) -and (![bool]$Module -or ($_.ImplementingModule -like $ModuleString))
}
$Resources += $keywords |
ForEach-Object -Process {
GetResourceFromKeyword -keyword $_ -patterns $patterns -modules $modules
} |
Where-Object -FilterScript {
$_ -ne $null
}
# Get composite resources
$Resources += Get-Command -CommandType Configuration |
ForEach-Object -Process {
GetCompositeResource $patterns $_ $ignoreResourceParameters -modules $modules
} |
Where-Object -FilterScript {
$_ -ne $null -and (![bool]$ModuleString -or ($_.Module -like $ModuleString)) -and
($_.Path -and ((Split-Path -Leaf $_.Path) -eq "$($_.Name).schema.psm1"))
}
# check whether all resources are found
CheckResourceFound $Name $Resources
}
catch
{
if ($initialized)
{
[System.Management.Automation.Language.DynamicKeyword]::Reset()
[Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ClearCache()
$initialized = $false
}
throw $_
}
}
End
{
$Resources = $Resources | Sort-Object -Property Module, Name
foreach ($resource in $Resources)
{
# return formatted string if required
if ($Syntax)
{
GetSyntax $resource | Write-Output
}
else
{
Write-Output -InputObject $resource
}
}
if ($initialized)
{
[System.Management.Automation.Language.DynamicKeyword]::Reset()
[Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::ClearCache()
$initialized = $false
}
}
}
#
# Get DSC resoruce for a dynamic keyword
#
function GetResourceFromKeyword
{
[OutputType('Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo')]
param (
[Parameter(Mandatory)]
[System.Management.Automation.Language.DynamicKeyword]
$keyword,
[System.Management.Automation.WildcardPattern[]]
$patterns,
[Parameter(Mandatory)]
[System.Management.Automation.PSModuleInfo[]]
$modules
)
# Find whether $name follows the pattern
$matched = (IsPatternMatched $patterns $keyword.ResourceName) -or (IsPatternMatched $patterns $keyword.Keyword)
if ($matched -eq $false)
{
$message = $LocalizedData.ResourceNotMatched -f @($keyword.Keyword)
Write-Verbose -Message ($message)
return
}
else
{
$message = $LocalizedData.CreatingResource -f @($keyword.Keyword)
Write-Verbose -Message $message
}
$resource = New-Object -TypeName Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo
$resource.ResourceType = $keyword.ResourceName
if ($keyword.ResourceName -ne $keyword.Keyword)
{
$resource.FriendlyName = $keyword.Keyword
}
$resource.Name = $keyword.Keyword
$schemaFiles = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::GetFileDefiningClass($keyword.ResourceName)
if ($schemaFiles.Count)
{
# Find the correct schema file that matches module name and version
# if same module/version is installed in multiple locations, then pick the first schema file.
foreach ($schemaFileName in $schemaFiles){
$moduleInfo = GetModule $modules $schemaFileName;
if ($moduleInfo.Name -eq $keyword.ImplementingModule -and $moduleInfo.Version -eq $keyword.ImplementingModuleVersion){
break
}
}
# if the class is not a resource we will ignore it except if it is DSC inbox resource.
if(-not $schemaFileName.StartsWith("$env:windir\system32\configuration",[stringComparison]::OrdinalIgnoreCase))
{
$classesFromSchema = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::GetCachedClassByFileName($schemaFileName)
if( $classesFromSchema -ne $null)
{
# check if the resource is proper DSC resource that always derives from OMI_BaseResource.
$schemaToProcess = $classesFromSchema | ForEach-Object -Process {
if(($_.CimSystemProperties.ClassName -ieq $keyword.ResourceName) -and ($_.CimSuperClassName -ieq 'OMI_BaseResource'))
{
$_
}
}
if( $schemaToProcess -eq $null)
{
return
}
}
}
$message = $LocalizedData.SchemaFileForResource -f @($schemaFileName)
Write-Verbose -Message $message
$resource.Module = $moduleInfo
$resource.Path = GetImplementingModulePath $schemaFileName
$resource.ParentPath = Split-Path $schemaFileName
}
else
{
$Module = $modules | Where-Object -FilterScript {
$_.Name -eq $keyword.ImplementingModule -and
$_.Version -eq $keyword.ImplementingModuleVersion
}
# Get cim classes defined under $Module
$cachedClasses = [Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache]::GetCachedClassesForModule($Module)
# Check if it is a DSC Resource that is always derived from OMI_BaseResource class.
$isDscResource = $false
foreach($cachedClasse in $cachedClasses) {
if($cachedClasse.CimSystemProperties.ClassName -ieq $keyword.Keyword -and ($cachedClasse.CimSuperClassName -ieq 'OMI_BaseResource'))
{
$isDscResource = $true
break
}
}
if(-not $isDscResource)
{
return
}
if ($Module -and $Module.ExportedDscResources -contains $keyword.Keyword)
{
$resource.Module = $Module
$resource.Path = $Module.Path
$resource.ParentPath = Split-Path -Path $Module.Path
}
}
if ([system.string]::IsNullOrEmpty($resource.Path) -eq $false)
{
$resource.ImplementedAs = [Microsoft.PowerShell.DesiredStateConfiguration.ImplementedAsType]::PowerShell
}
else
{
$resource.ImplementedAs = [Microsoft.PowerShell.DesiredStateConfiguration.ImplementedAsType]::Binary
}
if ($resource.Module -ne $null)
{
$resource.CompanyName = $resource.Module.CompanyName
}
# add properties
$keyword.Properties.Values | ForEach-Object -Process {
AddDscResourceProperty $resource $_
}
# sort properties
$updatedProperties = $resource.Properties | Sort-Object -Property @{
expression = 'IsMandatory'
Descending = $true
}, @{
expression = 'Name'
Ascending = $true
}
$resource.UpdateProperties($updatedProperties)
return $resource
}
#
# Gets composite resource
#
function GetCompositeResource
{
[OutputType('Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo')]
param (
[System.Management.Automation.WildcardPattern[]]
$patterns,
[Parameter(Mandatory)]
[System.Management.Automation.ConfigurationInfo]
$configInfo,
$ignoreParameters,
[Parameter(Mandatory)]
[System.Management.Automation.PSModuleInfo[]]
$modules
)
# Find whether $name follows the pattern
$matched = IsPatternMatched $patterns $configInfo.Name
if ($matched -eq $false)
{
$message = $LocalizedData.ResourceNotMatched -f @($configInfo.Name)
Write-Verbose -Message ($message)
return $null
}
else
{
$message = $LocalizedData.CreatingResource -f @($configInfo.Name)
Write-Verbose -Message $message
}
$resource = New-Object -TypeName Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo
$resource.ResourceType = $configInfo.Name
$resource.FriendlyName = $null
$resource.Name = $configInfo.Name
$resource.ImplementedAs = [Microsoft.PowerShell.DesiredStateConfiguration.ImplementedAsType]::Composite
if ($configInfo.Module -ne $null)
{
$resource.Module = GetModule $modules $configInfo.Module.Path
if($resource.Module -eq $null)
{
$resource.Module = $configInfo.Module
}
$resource.CompanyName = $configInfo.Module.CompanyName
$resource.Path = $configInfo.Module.Path
$resource.ParentPath = Split-Path -Path $resource.Path
}
# add properties
$configInfo.Parameters.Values | ForEach-Object -Process {
AddDscResourcePropertyFromMetadata $resource $_ $ignoreParameters
}
return $resource
}
#
# Adds property to a DSC resource
#
function AddDscResourceProperty
{
param (
[Parameter(Mandatory)]
[Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo]
$dscresource,
[Parameter(Mandatory)]
$property
)
$convertTypeMap = @{
'MSFT_Credential'='[PSCredential]';
'MSFT_KeyValuePair'='[HashTable]';
'MSFT_KeyValuePair[]'='[HashTable]'
}
$ignoreProperties = @('ResourceId', 'ConfigurationName')
if ($ignoreProperties -contains $property.Name)
{
return
}
$dscProperty = New-Object -TypeName Microsoft.PowerShell.DesiredStateConfiguration.DscResourcePropertyInfo
$dscProperty.Name = $property.Name
if ($convertTypeMap.ContainsKey($property.TypeConstraint))
{
$type = $convertTypeMap[$property.TypeConstraint]
}
else
{
$Type = [System.Management.Automation.LanguagePrimitives]::ConvertTypeNameToPSTypeName($property.TypeConstraint)
if([string]::IsNullOrEmpty($Type))
{
$Type = "[$($property.TypeConstraint)]"
}
}
if ($property.ValueMap -ne $null)
{
$property.ValueMap.Keys |
Sort-Object |
ForEach-Object -Process {
$dscProperty.Values.Add($_)
}
}
$dscProperty.PropertyType = $Type
$dscProperty.IsMandatory = $property.Mandatory
$dscresource.Properties.Add($dscProperty)
}
#
# Adds property to a DSC resource
#
function AddDscResourcePropertyFromMetadata
{
param (
[Parameter(Mandatory)]
[Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo]
$dscresource,
[Parameter(Mandatory)]
[System.Management.Automation.ParameterMetadata]
$parameter,
$ignoreParameters
)
if ($ignoreParameters -contains $parameter.Name)
{
return
}
$dscProperty = New-Object -TypeName Microsoft.PowerShell.DesiredStateConfiguration.DscResourcePropertyInfo
$dscProperty.Name = $parameter.Name
# adding [] in Type name to keep it in sync with the name returned from LanguagePrimitives.ConvertTypeNameToPSTypeName
$dscProperty.PropertyType = '[' +$parameter.ParameterType.Name + ']'
$dscProperty.IsMandatory = $parameter.Attributes.Mandatory
$dscresource.Properties.Add($dscProperty)
}
#
# Gets syntax for a DSC resource
#
function GetSyntax
{
[OutputType('string')]
param (
[Parameter(Mandatory)]
[Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo]
$dscresource
)
$output = $dscresource.Name + " [String] #ResourceName`n"
$output += "{`n"
foreach ($property in $dscresource.Properties)
{
$output += ' '
if ($property.IsMandatory -eq $false)
{
$output += '['
}
$output += $property.Name
$output += ' = ' + $property.PropertyType + ''
# Add possible values
if ($property.Values.Count -gt 0)
{
$output += '{ ' + [system.string]::Join(' | ', $property.Values) + ' }'
}
if ($property.IsMandatory -eq $false)
{
$output += ']'
}
$output += "`n"
}
$output += "}`n"
return $output
}
#
# Checks whether a resource is found or not
#
function CheckResourceFound($names, $Resources)
{
if ($names -eq $null)
{
return
}
$namesWithoutWildcards = $names | Where-Object -FilterScript {
[System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters($_) -eq $false
}
foreach ($Name in $namesWithoutWildcards)
{
$foundResources = $Resources | Where-Object -FilterScript {
($_.Name -eq $Name) -or ($_.ResourceType -eq $Name)
}
if ($foundResources.Count -eq 0)
{
$errorMessage = $LocalizedData.ResourceNotFound -f @($Name, 'Resource')
Write-Error -Message $errorMessage
}
}
}
#
# Get implementing module path
#
function GetImplementingModulePath
{
param (
[Parameter(Mandatory)]
[string]
$schemaFileName
)
$moduleFileName = ($schemaFileName -replace ".schema.mof$", '') + '.psd1'
if (Test-Path $moduleFileName)
{
return $moduleFileName
}
$moduleFileName = ($schemaFileName -replace ".schema.mof$", '') + '.psm1'
if (Test-Path $moduleFileName)
{
return $moduleFileName
}
return
}
#
# Gets module for a DSC resource
#
function GetModule
{
[OutputType('System.Management.Automation.PSModuleInfo')]
param (
[Parameter(Mandatory)]
[System.Management.Automation.PSModuleInfo[]]
$modules,
[Parameter(Mandatory)]
[string]
$schemaFileName
)
if($schemaFileName -eq $null)
{
return $null
}
$schemaFileExt = $null
if ($schemaFileName -match '.schema.mof')
{
$schemaFileExt = ".schema.mof$"
}
if ($schemaFileName -match '.schema.psm1')
{
$schemaFileExt = ".schema.psm1$"
}
if(!$schemaFileExt)
{
return $null
}
# get module from parent directory.
# Desired structure is : <Module-directory>/DSCResources/<schema file directory>/schema.File
$validResource = $false
$schemaDirectory = Split-Path $schemaFileName
if($schemaDirectory)
{
$subDirectory = [System.IO.Directory]::GetParent($schemaDirectory)
if ($subDirectory -and ($subDirectory.Name -eq 'DSCResources') -and $subDirectory.Parent)
{
$moduleDirectory = [System.IO.Directory]::GetParent($subDirectory)
$results = $modules | Where-Object -FilterScript {
$_.ModuleBase -eq $subDirectory.Parent.FullName
}
if ($results)
{
# Log Resource is internally handled by the CA. There is no formal provider for it.
if ($schemaFileName -match 'MSFT_LogResource')
{
$validResource = $true
}
else
{
# check for proper resource module
foreach ($ext in @('.psd1', '.psm1', '.dll', '.cdxml'))
{
$resModuleFileName = ($schemaFileName -replace $schemaFileExt, '') + $ext
if(Test-Path($resModuleFileName))
{
$validResource = $true
break
}
}
}
}
}
}
if ($results -and $validResource)
{
return $results[0]
}
else
{
return $null
}
}
#
# Checks whether a resource is hidden or not
#
function IsHiddenResource
{
param (
[Parameter(Mandatory)]
[string]
$ResourceName
)
$hiddenResources = @(
'OMI_BaseResource',
'MSFT_KeyValuePair',
'MSFT_BaseConfigurationProviderRegistration',
'MSFT_CimConfigurationProviderRegistration',
'MSFT_PSConfigurationProviderRegistration',
'OMI_ConfigurationDocument',
'MSFT_Credential',
'MSFT_DSCMetaConfiguration',
'OMI_ConfigurationDownloadManager',
'OMI_ResourceModuleManager',
'OMI_ReportManager',
'MSFT_FileDownloadManager',
'MSFT_WebDownloadManager',
'MSFT_FileResourceManager',
'MSFT_WebResourceManager',
'MSFT_WebReportManager',
'OMI_MetaConfigurationResource',
'MSFT_PartialConfiguration',
'MSFT_DSCMetaConfigurationV2'
)
return $hiddenResources -contains $ResourceName
}
#
# Gets patterns for names
#
function GetPatterns
{
[OutputType('System.Management.Automation.WildcardPattern[]')]
param (
[string[]]
$names
)
$patterns = @()
if ($names -eq $null)
{
return $patterns
}
foreach ($Name in $names)
{
$patterns += New-Object -TypeName System.Management.Automation.WildcardPattern -ArgumentList @($Name, [System.Management.Automation.WildcardOptions]::IgnoreCase)
}
return $patterns
}
#
# Checks whether an input name matches one of the patterns
# $pattern is not expected to have an empty or null values
#
function IsPatternMatched
{
[OutputType('bool')]
param (
[System.Management.Automation.WildcardPattern[]]
$patterns,
[Parameter(Mandatory)]
[string]
$Name
)
if ($patterns -eq $null)
{
return $true
}
foreach ($pattern in $patterns)
{
if ($pattern.IsMatch($Name))
{
return $true
}
}
return $false
}
Export-ModuleMember -Function Get-DscResource
###########################################################
###########################################################
# Aliases
###########################################################
New-Alias -Name 'sacfg' -Value 'Start-DSCConfiguration'
New-Alias -Name 'tcfg' -Value 'Test-DSCConfiguration'
New-Alias -Name 'gcfg' -Value 'Get-DSCConfiguration'
New-Alias -Name 'rtcfg' -Value 'Restore-DSCConfiguration'
New-Alias -Name 'glcm' -Value 'Get-DSCLocalConfigurationManager'
New-Alias -Name 'slcm' -Value 'Set-DSCLocalConfigurationManager'
New-Alias -Name 'pbcfg' -Value 'Publish-DSCConfiguration'
New-Alias -Name 'ulcm' -Value 'Update-DscLocalConfigurationManager'
New-Alias -Name 'upcfg' -Value 'Update-DSCConfiguration'
New-Alias -Name 'gcfgs' -Value 'Get-DscConfigurationStatus'
Export-ModuleMember -Alias * -Function *
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment