/// <summary> /// Routine to format a usage string from keyword. The resulting string should look like: /// User [string] #ResourceName /// { /// UserName = [string] /// [ Description = [string] ] /// [ Disabled = [bool] ] /// [ Ensure = [string] { Absent | Present } ] /// [ Force = [bool] ] /// [ FullName = [string] ] /// [ Password = [PSCredential] ] /// [ PasswordChangeNotAllowed = [bool] ] /// [ PasswordChangeRequired = [bool] ] /// [ PasswordNeverExpires = [bool] ] /// [ DependsOn = [string[]] ] /// } /// </summary> /// <param name="keyword"></param> /// <returns></returns> public static string GetDSCResourceUsageString(DynamicKeyword keyword) { StringBuilder usageString; switch (keyword.NameMode) { // Name must be present and simple non-empty bare word case DynamicKeywordNameMode.SimpleNameRequired: usageString = new StringBuilder(keyword.Keyword + " [string] # Resource Name"); break; // Name must be present but can also be an expression case DynamicKeywordNameMode.NameRequired: usageString = new StringBuilder(keyword.Keyword + " [string[]] # Name List"); break; // Name may be optionally present, but if it is present, it must be a non-empty bare word. case DynamicKeywordNameMode.SimpleOptionalName: usageString = new StringBuilder(keyword.Keyword + " [ [string] ] # Optional Name"); break; // Name may be optionally present, expression or bare word case DynamicKeywordNameMode.OptionalName: usageString = new StringBuilder(keyword.Keyword + " [ [string[]] ] # Optional NameList"); break; // Does not take a name default: usageString = new StringBuilder(keyword.Keyword); break; } usageString.Append("\n{\n"); bool listKeyProperties = true; while (true) { foreach (var prop in keyword.Properties.OrderBy(ob => ob.Key)) { if (string.Equals(prop.Key, "ResourceId", StringComparison.OrdinalIgnoreCase)) { continue; } var propVal = prop.Value; if (listKeyProperties && propVal.IsKey || !listKeyProperties && !propVal.IsKey) { usageString.Append(propVal.Mandatory ? " " : " [ "); usageString.Append(prop.Key); usageString.Append(" = "); usageString.Append(FormatCimPropertyType(propVal, !propVal.Mandatory)); } } if (listKeyProperties) { listKeyProperties = false; } else { break; } } usageString.Append("}"); return usageString.ToString(); }
/// <summary> /// Load the default system CIM classes and create the corresponding keywords. /// </summary> /// <param name="functionsToDefine">A dictionary to add the defined functions to, may be null</param> /// <param name="errors">Collection of any errors encountered while loading keywords.</param> /// <param name="modulePathList">List of module path from where DSC PS modules will be loaded</param> /// <param name="cacheResourcesFromMultipleModuleVersions">Allow caching the resources from multiple versions of modules</param> private static void LoadDefaultCimKeywords(Dictionary<string, ScriptBlock> functionsToDefine, Collection<Exception> errors, List<string> modulePathList, bool cacheResourcesFromMultipleModuleVersions) { DynamicKeyword.Reset(); Initialize(errors, modulePathList); // Initialize->ClearCache resets CacheResourcesFromMultipleModuleVersions to false, // workaround is to set it after Initialize method call. // Initialize method imports all the Inbox resources and internal classes which belongs to only one version // of the module, so it is ok if this property is not set during cache initialization. CacheResourcesFromMultipleModuleVersions = cacheResourcesFromMultipleModuleVersions; foreach (var cimClass in GetCachedClasses()) { var className = cimClass.Item2.CimSystemProperties.ClassName; var moduleInfo = ByClassModuleCache[className]; CreateAndRegisterKeywordFromCimClass(moduleInfo.Item1, moduleInfo.Item2, cimClass.Item2, functionsToDefine, cimClass.Item1); } // And add the Node keyword definitions if (!DynamicKeyword.ContainsKeyword("Node")) { // Implement dispatch to the Node keyword. var nodeKeyword = new DynamicKeyword() { BodyMode = DynamicKeywordBodyMode.ScriptBlock, ImplementingModule = s_defaultModuleInfoForResource.Item1, ImplementingModuleVersion = s_defaultModuleInfoForResource.Item2, NameMode = DynamicKeywordNameMode.NameRequired, Keyword = "Node", }; DynamicKeyword.AddKeyword(nodeKeyword); } // And add the Import-DscResource keyword definitions if (!DynamicKeyword.ContainsKeyword("Import-DscResource")) { // Implement dispatch to the Node keyword. var nodeKeyword = new DynamicKeyword() { BodyMode = DynamicKeywordBodyMode.Command, ImplementingModule = s_defaultModuleInfoForResource.Item1, ImplementingModuleVersion = s_defaultModuleInfoForResource.Item2, NameMode = DynamicKeywordNameMode.NoName, Keyword = "Import-DscResource", MetaStatement = true, PostParse = ImportResourcePostParse, SemanticCheck = ImportResourceCheckSemantics }; DynamicKeyword.AddKeyword(nodeKeyword); } }
/// <summary> /// A method to generate a keyword from a CIM class object. This is used for DSC. /// </summary> /// <param name="moduleName"></param> /// <param name="moduleVersion"></param> /// <param name="cimClass"></param> /// <param name="functionsToDefine">If true, don't define the keywords, just create the functions.</param> /// <param name="runAsBehavior"> To specify RunAs behavior of the class </param> private static DynamicKeyword CreateKeywordFromCimClass(string moduleName, Version moduleVersion, Microsoft.Management.Infrastructure.CimClass cimClass, Dictionary<string, ScriptBlock> functionsToDefine, DSCResourceRunAsCredential runAsBehavior) { var resourceName = cimClass.CimSystemProperties.ClassName; string alias = GetFriendlyName(cimClass); var keywordString = string.IsNullOrEmpty(alias) ? resourceName : alias; // // Skip all of the base, meta, registration and other classes that are not intended to be used directly by a script author // if (System.Text.RegularExpressions.Regex.Match(keywordString, "^OMI_Base|^OMI_.*Registration", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success) { return null; } var keyword = new DynamicKeyword() { BodyMode = DynamicKeywordBodyMode.Hashtable, Keyword = keywordString, ResourceName = resourceName, ImplementingModule = moduleName, ImplementingModuleVersion = moduleVersion, SemanticCheck = CheckMandatoryPropertiesPresent }; // If it's one of reserved dynamic keyword, mark it if (System.Text.RegularExpressions.Regex.Match(keywordString, reservedDynamicKeywords, System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success) { keyword.IsReservedKeyword = true; } // see if it's a resource type i.e. it inherits from OMI_BaseResource bool isResourceType = false; for (var classToCheck = cimClass; !string.IsNullOrEmpty(classToCheck.CimSuperClassName); classToCheck = classToCheck.CimSuperClass) { if (string.Equals("OMI_BaseResource", classToCheck.CimSuperClassName, StringComparison.OrdinalIgnoreCase) || string.Equals("OMI_MetaConfigurationResource", classToCheck.CimSuperClassName, StringComparison.OrdinalIgnoreCase)) { isResourceType = true; break; } } // If it's a resource type, then a resource name is required. keyword.NameMode = isResourceType ? DynamicKeywordNameMode.NameRequired : DynamicKeywordNameMode.NoName; // // Add the settable properties to the keyword object // foreach (var prop in cimClass.CimClassProperties) { // If the property is marked as readonly, skip it... if ((prop.Flags & Microsoft.Management.Infrastructure.CimFlags.ReadOnly) == Microsoft.Management.Infrastructure.CimFlags.ReadOnly) { continue; } try { // If the property has the Read qualifier, also skip it. if (prop.Qualifiers["Read"] != null) { continue; } } catch (Microsoft.Management.Infrastructure.CimException) { // Cim exception means Read wasn't found so continue... } // If it's one of our magic properties, skip it if (IsMagicProperty(prop.Name)) { continue; } if (runAsBehavior == DSCResourceRunAsCredential.NotSupported) { if (String.Equals(prop.Name, "PsDscRunAsCredential", StringComparison.OrdinalIgnoreCase)) { // skip adding PsDscRunAsCredential to the dynamic word for the dsc resource. continue; } } // If it's one of our reserved properties, save it for error reporting if (System.Text.RegularExpressions.Regex.Match(prop.Name, reservedProperties, System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success) { keyword.HasReservedProperties = true; continue; } // Otherwise, add it to the Keyword List. var keyProp = new System.Management.Automation.Language.DynamicKeywordProperty(); keyProp.Name = prop.Name; // Set the mandatory flag if appropriate if ((prop.Flags & Microsoft.Management.Infrastructure.CimFlags.Key) == Microsoft.Management.Infrastructure.CimFlags.Key) { keyProp.Mandatory = true; keyProp.IsKey = true; } // Copy the type name string. If it's an embedded instance, need to grab it from the ReferenceClassName bool referenceClassNameIsNullOrEmpty = string.IsNullOrEmpty(prop.ReferenceClassName); if (prop.CimType == CimType.Instance && !referenceClassNameIsNullOrEmpty) { keyProp.TypeConstraint = prop.ReferenceClassName; } else if (prop.CimType == CimType.InstanceArray && !referenceClassNameIsNullOrEmpty) { keyProp.TypeConstraint = prop.ReferenceClassName + "[]"; } else { keyProp.TypeConstraint = prop.CimType.ToString(); } string[] valueMap = null; foreach (var qualifier in prop.Qualifiers) { // Check to see if there is a Values attribute and save the list of allowed values if so. if (string.Equals(qualifier.Name, "Values", StringComparison.OrdinalIgnoreCase) && qualifier.CimType == Microsoft.Management.Infrastructure.CimType.StringArray) { keyProp.Values.AddRange((string[])qualifier.Value); } // Check to see if there is a ValueMap attribute and save the list of allowed values if so. if (string.Equals(qualifier.Name, "ValueMap", StringComparison.OrdinalIgnoreCase) && qualifier.CimType == Microsoft.Management.Infrastructure.CimType.StringArray) { valueMap = (string[])qualifier.Value; } // Check to see if this property has the Required qualifier associated with it. if (string.Equals(qualifier.Name, "Required", StringComparison.OrdinalIgnoreCase) && qualifier.CimType == Microsoft.Management.Infrastructure.CimType.Boolean && (bool)qualifier.Value) { keyProp.Mandatory = true; } // set the property to mandatory is specified for the resource. if (runAsBehavior == DSCResourceRunAsCredential.Mandatory) { if (String.Equals(prop.Name, "PsDscRunAsCredential", StringComparison.OrdinalIgnoreCase)) { keyProp.Mandatory = true; } } } if (valueMap != null && keyProp.Values.Count > 0) { if (valueMap.Length != keyProp.Values.Count) { s_tracer.WriteLine( "DSC CreateDynamicKeywordFromClass: the count of values for qualifier 'Values' and 'ValueMap' doesn't match. count of 'Values': {0}, count of 'ValueMap': {1}. Skip the keyword '{2}'.", keyProp.Values.Count, valueMap.Length, keyword.Keyword); return null; } for (int index = 0; index < valueMap.Length; index++) { string key = keyProp.Values[index]; string value = valueMap[index]; if (keyProp.ValueMap.ContainsKey(key)) { s_tracer.WriteLine( "DSC CreateDynamicKeywordFromClass: same string value '{0}' appears more than once in qualifier 'Values'. Skip the keyword '{1}'.", key, keyword.Keyword); return null; } keyProp.ValueMap.Add(key, value); } } keyword.Properties.Add(prop.Name, keyProp); } // update specific keyword with range constraints UpdateKnownRestriction(keyword); return keyword; }
/// <summary> /// update range restriction for meta configuration keywords /// the restrictions are for /// ConfigurationModeFrequency: 15-44640 /// RefreshFrequency: 30-44640 /// </summary> /// <param name="keyword"></param> private static void UpdateKnownRestriction(DynamicKeyword keyword) { if ( string.Compare(keyword.ResourceName, "MSFT_DSCMetaConfigurationV2", StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(keyword.ResourceName, "MSFT_DSCMetaConfiguration", StringComparison.OrdinalIgnoreCase) == 0) { if (keyword.Properties["RefreshFrequencyMins"] != null) { keyword.Properties["RefreshFrequencyMins"].Range = new Tuple<int, int>(30, 44640); } if (keyword.Properties["ConfigurationModeFrequencyMins"] != null) { keyword.Properties["ConfigurationModeFrequencyMins"].Range = new Tuple<int, int>(15, 44640); } if (keyword.Properties["DebugMode"] != null) { keyword.Properties["DebugMode"].Values.Remove("ResourceScriptBreakAll"); keyword.Properties["DebugMode"].ValueMap.Remove("ResourceScriptBreakAll"); } } }
/// <summary> /// Parse a dynamic keyword statement which will be either of the form /// keyword [parameters] [name] { a=1; b=2; } # constructor with properties /// or /// keyword [parameters] [name] { ... } # constructor with a simple body. /// or keywordcommand parameters /// This custom keyword does not introduce a new AST node type. Instead it generates a /// CommandAst that calls a PowerShell command to implement the keyword's logic. /// This command has one of two signatures: /// keywordImplCommand /// </summary> /// <param name="functionName">The name of the function to invoke</param> /// <param name="keywordData">The data for this keyword definition</param> /// <returns></returns> private StatementAst DynamicKeywordStatementRule(Token functionName, DynamicKeyword keywordData) { ////////////////////////////////////////////////////////////////////////////////// // If a custom action was provided. then invoke it ////////////////////////////////////////////////////////////////////////////////// if (keywordData.PreParse != null) { try { ParseError[] errors = keywordData.PreParse(keywordData); if (errors != null && errors.Length > 0) { foreach (var e in errors) { ReportError(e); } } } catch (Exception e) { ReportError(functionName.Extent, () => ParserStrings.DynamicKeywordPreParseException, keywordData.ResourceName, e.ToString()); return null; } } if (keywordData.IsReservedKeyword) { // ErrorRecovery: eat the token ReportError(functionName.Extent, () => ParserStrings.UnsupportedReservedKeyword, keywordData.Keyword); return null; } if (keywordData.HasReservedProperties) { // ErrorRecovery: eat the token ReportError(functionName.Extent, () => ParserStrings.UnsupportedReservedProperty, "'Require', 'Trigger', 'Notify', 'Before', 'After' and 'Subscribe'"); return null; } string elementName = string.Empty; DynamicKeywordStatementAst dynamicKeywordAst; if (keywordData.BodyMode == DynamicKeywordBodyMode.Command) { UngetToken(functionName); dynamicKeywordAst = (DynamicKeywordStatementAst)CommandRule(forDynamicKeyword: true); dynamicKeywordAst.Keyword = keywordData; dynamicKeywordAst.FunctionName = functionName; } else { SkipNewlines(); // The expression that returns the resource name or names. ExpressionAst instanceName = null; Token nameToken = NextToken(); if (nameToken.Kind == TokenKind.EndOfInput) { UngetToken(nameToken); if (keywordData.NameMode == DynamicKeywordNameMode.NameRequired || keywordData.NameMode == DynamicKeywordNameMode.SimpleNameRequired) { ReportIncompleteInput(After(functionName), () => ParserStrings.RequiredNameOrExpressionMissing); } else { // Name not required so report missing brace ReportIncompleteInput(After(functionName.Extent), () => ParserStrings.MissingBraceInObjectDefinition); } return null; } // If it's an lcurly, then no name was provided, and we skip to the body processing Token lCurly = null; if (nameToken.Kind == TokenKind.LCurly) { lCurly = nameToken; if (keywordData.NameMode == DynamicKeywordNameMode.NameRequired || keywordData.NameMode == DynamicKeywordNameMode.SimpleNameRequired) { ReportError(After(functionName), () => ParserStrings.RequiredNameOrExpressionMissing); UngetToken(nameToken); return null; } } else if (nameToken.Kind == TokenKind.Identifier || nameToken.Kind == TokenKind.DynamicKeyword) { if (keywordData.NameMode == DynamicKeywordNameMode.NoName) { ReportError(After(functionName), () => ParserStrings.UnexpectedNameForType, functionName.Text, nameToken.Text); UngetToken(nameToken); return null; } // If it's an identifier then this is the name for the data object elementName = nameToken.Text; // If only a simple name is allowed, then the string must be non-null. if ((keywordData.NameMode == DynamicKeywordNameMode.SimpleNameRequired || keywordData.NameMode == DynamicKeywordNameMode.SimpleOptionalName) && string.IsNullOrEmpty(elementName)) { ReportIncompleteInput(After(functionName), () => ParserStrings.RequiredNameOrExpressionMissing); UngetToken(nameToken); return null; } } else { // see if an expression was provided instead of a bare word... UngetToken(nameToken); instanceName = GetSingleCommandArgument(CommandArgumentContext.CommandName); if (instanceName == null) { if (keywordData.NameMode == DynamicKeywordNameMode.SimpleNameRequired || keywordData.NameMode == DynamicKeywordNameMode.SimpleOptionalName) { ReportError(After(functionName), () => ParserStrings.RequiredNameOrExpressionMissing); } else { // It wasn't an '{' and it wasn't a name expression so it's a unexpected token. ReportError(After(functionName), () => ParserStrings.UnexpectedToken, nameToken.Text); } return null; } // Ok, we got a name expression, but we're expecting no name, so it's and error. if (keywordData.NameMode == DynamicKeywordNameMode.NoName) { ReportError(After(functionName), () => ParserStrings.UnexpectedNameForType, functionName.Text, instanceName.ToString()); return null; } // We were expecting a simple name so report an error if (keywordData.NameMode == DynamicKeywordNameMode.SimpleNameRequired || keywordData.NameMode == DynamicKeywordNameMode.SimpleOptionalName) { // If no match, then this is an incomplete token BUGBUG fix message ReportError(nameToken.Extent, () => ParserStrings.UnexpectedToken, nameToken.Text); return null; } } // If we didn't get a resource expression AST, then we need to build one out of the // name that was specified. It may be the case that we don't have // a resource name in which case it will be the empty string. Even in the cases were // we aren't expecting a name, we still do this so that the signature of the implementing function remains // the same. ExpressionAst originalInstanceName = instanceName; if (instanceName == null) { instanceName = new StringConstantExpressionAst(nameToken.Extent, elementName, StringConstantType.BareWord); } SkipNewlines(); // // Now look for the body of the data statement. // if (lCurly == null) { lCurly = NextToken(); if (lCurly.Kind == TokenKind.EndOfInput) { UngetToken(lCurly); ReportIncompleteInput(After(functionName.Extent), () => ParserStrings.MissingBraceInObjectDefinition); // Preserve the name expression for tab completion return originalInstanceName == null ? null : new ErrorStatementAst(ExtentOf(functionName, originalInstanceName), GetNestedErrorAsts(originalInstanceName)); } if (lCurly.Kind != TokenKind.LCurly) { // We need to generate a reasonable error message for this case: // // Configuration C { // node $AllNode.NodeName{ # There is no space before curly, and we are converting scriptblock to and argument to call 'NodeName' // ... // } // } # we don't want to simple report an unexpected token here, it would be super-confusing. InvokeMemberExpressionAst instanceInvokeMemberExpressionAst = instanceName as InvokeMemberExpressionAst; if (instanceInvokeMemberExpressionAst != null && instanceInvokeMemberExpressionAst.Arguments.Count == 1 && instanceInvokeMemberExpressionAst.Arguments[0] is ScriptBlockExpressionAst && // the last condition checks that there is no space between "method" name and '{' instanceInvokeMemberExpressionAst.Member.Extent.EndOffset == instanceInvokeMemberExpressionAst.Arguments[0].Extent.StartOffset) { ReportError(LastCharacterOf(instanceInvokeMemberExpressionAst.Member.Extent), () => ParserStrings.UnexpectedTokenInDynamicKeyword, functionName.Text); } else { ReportError(lCurly.Extent, () => ParserStrings.UnexpectedToken, lCurly.Text); } if (lCurly.Kind == TokenKind.Dot && originalInstanceName != null && lCurly.Extent.StartOffset == originalInstanceName.Extent.EndOffset) { // Generate more useful ast for tab-completing extension methods on special DSC collection variables // e.g. configuration foo { node $AllNodes.<tab> IScriptExtent errorExtent = ExtentOf(originalInstanceName, lCurly); var errorExpr = new ErrorExpressionAst(errorExtent); var memberExpr = new MemberExpressionAst(originalInstanceName.Extent, originalInstanceName, errorExpr, @static: false); return new ErrorStatementAst(errorExtent, new[] { memberExpr }); } UngetToken(lCurly); // Preserve the name expression for tab completion return originalInstanceName == null ? null : new ErrorStatementAst(ExtentOf(functionName, originalInstanceName), GetNestedErrorAsts(originalInstanceName)); } } // // The keyword data is used to see // if a scriptblock or a hashtable is expected. // ExpressionAst body = null; if (keywordData.BodyMode == DynamicKeywordBodyMode.ScriptBlock) { var oldInConfiguraiton = _inConfiguration; try { _inConfiguration = false; body = ScriptBlockExpressionRule(lCurly); } finally { _inConfiguration = oldInConfiguraiton; } } else if (keywordData.BodyMode == DynamicKeywordBodyMode.Hashtable) { // Resource property value could be set to nested DSC resources except Script resource bool isScriptResource = String.Compare(functionName.Text, @"Script", StringComparison.OrdinalIgnoreCase) == 0; try { if (isScriptResource) DynamicKeyword.Push(); body = HashExpressionRule(lCurly, true /* parsingSchemaElement */); } finally { if (isScriptResource) DynamicKeyword.Pop(); } } // commandast // elements: instancename/dynamickeyword/hashtable or scripblockexpress if (body == null) { // Failed to read the statement body ReportIncompleteInput(After(lCurly), () => ParserStrings.MissingStatementAfterKeyword, keywordData.Keyword); // Preserve the name expression for tab completion return originalInstanceName == null ? null : new ErrorStatementAst(ExtentOf(functionName, originalInstanceName), GetNestedErrorAsts(originalInstanceName)); } ////////////////////////////////////////////////////////////////////////// // The statement is now fully parsed ////////////////////////////////////////////////////////////////////////// // // Create DynamicKeywordStatementAst // Collection<CommandElementAst> commandElements = new Collection<CommandElementAst> { new StringConstantExpressionAst(functionName.Extent, functionName.Text, StringConstantType.BareWord), (ExpressionAst)instanceName.Copy(), (ExpressionAst)body.Copy() }; Token nextToken = NextToken(); IScriptExtent dynamicKeywordExtent = ExtentOf(functionName, Before(nextToken)); UngetToken(nextToken); dynamicKeywordAst = new DynamicKeywordStatementAst(dynamicKeywordExtent, commandElements) { Keyword = keywordData, LCurly = lCurly, FunctionName = functionName, InstanceName = instanceName, OriginalInstanceName = originalInstanceName, BodyExpression = body, ElementName = elementName, }; } ////////////////////////////////////////////////////////////////////////////////// // If a custom action was provided. then invoke it ////////////////////////////////////////////////////////////////////////////////// if (keywordData.PostParse != null) { try { ParseError[] errors = keywordData.PostParse(dynamicKeywordAst); if (errors != null && errors.Length > 0) { foreach (var e in errors) { ReportError(e); } } } catch (Exception e) { ReportError(functionName.Extent, () => ParserStrings.DynamicKeywordPostParseException, keywordData.Keyword, e.ToString()); return null; } } return dynamicKeywordAst; }