Created
February 28, 2017 22:40
-
-
Save CJHarmath/f2918d4f0dc7e14bdff1fa579b49ebbb to your computer and use it in GitHub Desktop.
try catch workaround for PSDscResources/issues/43
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
########################################################### | |
# | |
# '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