diff --git a/src/DscSubsystem/CrossPlatformDsc.cs b/src/DscSubsystem/CrossPlatformDsc.cs new file mode 100644 index 0000000..ebad8d0 --- /dev/null +++ b/src/DscSubsystem/CrossPlatformDsc.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.ObjectModel; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Internal; +using System.Management.Automation.Language; + +namespace System.Management.Automation.Subsystem +{ + /// + /// Interface for implementing a cross platform desired state configuration component. + /// + public class CrossPlatformDsc : ICrossPlatformDsc, IModuleAssemblyInitializer + { + /// + /// Gets the unique identifier for a subsystem implementation. + /// + public Guid Id { get {return Guid.NewGuid();} } + + /// + /// Gets the name of a subsystem implementation. + /// + public string Name { get {return "Cross platform desired state configuration";} } + + /// + /// Gets the description of a subsystem implementation. + /// + public string Description { get {return "Cross platform desired state configuration";} } + + /// + /// Gets a dictionary that contains the functions to be defined at the global scope of a PowerShell session. + /// Key: function name; Value: function script. + /// + Dictionary? ISubsystem.FunctionsToDefine => null; + + /// + /// Test. + /// + public void LoadDefaultCimKeywords(Collection errors) + { + Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.LoadDefaultCimKeywords(errors); + } + + /// + /// Default summary. + /// + public void ClearCache() + { + Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.ClearCache(); + } + + /// + /// Default summary. + /// + public bool NewApiIsUsed + { + get + { + return Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.NewApiIsUsed; + } + } + + /// + /// Default summary. + /// + public string GetDSCResourceUsageString(DynamicKeyword keyword) + { + return Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache.GetDSCResourceUsageString(keyword); + } + + /// + /// Test. + /// + public void RegisterSubsystemIfNotAlready() + { + if (SubsystemManager.GetSubsystem() == null) + { + SubsystemManager.RegisterSubsystem(SubsystemKind.CrossPlatformDsc, this); + } + } + + public void OnImport() + { + RegisterSubsystemIfNotAlready(); + } + } +} diff --git a/src/DscSubsystem/JsonCimDSCParser.cs b/src/DscSubsystem/JsonCimDSCParser.cs new file mode 100644 index 0000000..c2221cb --- /dev/null +++ b/src/DscSubsystem/JsonCimDSCParser.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Security; + +namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform +{ + /// + /// Class that does high level Cim schema parsing. + /// + internal class CimDSCParser + { + private readonly JsonDeserializer _jsonDeserializer; + + internal CimDSCParser() + { + _jsonDeserializer = JsonDeserializer.Create(); + } + + internal IEnumerable ParseSchemaJson(string filePath, bool useNewRunspace = false) + { + try + { + string json = File.ReadAllText(filePath); + string fileNameDefiningClass = Path.GetFileNameWithoutExtension(filePath); + int dotIndex = fileNameDefiningClass.IndexOf(".schema", StringComparison.InvariantCultureIgnoreCase); + if (dotIndex != -1) + { + fileNameDefiningClass = fileNameDefiningClass.Substring(0, dotIndex); + } + + IEnumerable result = _jsonDeserializer.DeserializeClasses(json, useNewRunspace); + foreach (dynamic classObject in result) + { + string superClassName = classObject.SuperClassName; + string className = classObject.ClassName; + if (string.Equals(superClassName, "OMI_BaseResource", StringComparison.OrdinalIgnoreCase)) + { + // Get the name of the file without schema.mof/json extension + if (!className.Equals(fileNameDefiningClass, StringComparison.OrdinalIgnoreCase)) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( + ParserStrings.ClassNameNotSameAsDefiningFile, className, fileNameDefiningClass); + throw e; + } + } + } + + return result; + } + catch (Exception exception) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( + exception, ParserStrings.CimDeserializationError, filePath); + + e.SetErrorId("CimDeserializationError"); + throw e; + } + } + } +} diff --git a/src/DscSubsystem/JsonDeserializer.cs b/src/DscSubsystem/JsonDeserializer.cs new file mode 100644 index 0000000..319560f --- /dev/null +++ b/src/DscSubsystem/JsonDeserializer.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Runspaces; + +namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform +{ + internal class JsonDeserializer + { + #region Constructors + + /// + /// Instantiates a default deserializer. + /// + /// Default deserializer. + public static JsonDeserializer Create() + { + return new JsonDeserializer(); + } + + #endregion Constructors + + #region Methods + + /// + /// Returns schema of Cim classes from specified json file. + /// + /// Json text to deserialize. + /// If a new runspace should be used. + /// Deserialized PSObjects. + public IEnumerable DeserializeClasses(string json, bool useNewRunspace = false) + { + if (string.IsNullOrEmpty(json)) + { + throw new ArgumentNullException(nameof(json)); + } + + System.Management.Automation.PowerShell powerShell = null; + + if (useNewRunspace) + { + // currently using RunspaceMode.NewRunspace will reset PSModulePath env var for the entire process + // this is something we want to avoid in DSC GuestConfigAgent scenario, so we use following workaround + var s_iss = InitialSessionState.CreateDefault(); + s_iss.EnvironmentVariables.Add( + new SessionStateVariableEntry( + "PSModulePath", + Environment.GetEnvironmentVariable("PSModulePath"), + description: null)); + powerShell = System.Management.Automation.PowerShell.Create(s_iss); + } + else + { + powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); + } + + using (powerShell) + { + return powerShell.AddCommand("Microsoft.PowerShell.Utility\\ConvertFrom-Json") + .AddParameter("InputObject", json) + .AddParameter("Depth", 100) // maximum supported by cmdlet + .Invoke(); + } + } + + #endregion Methods + } +} diff --git a/src/DscSubsystem/JsonDscClassCache.cs b/src/DscSubsystem/JsonDscClassCache.cs new file mode 100644 index 0000000..15576a0 --- /dev/null +++ b/src/DscSubsystem/JsonDscClassCache.cs @@ -0,0 +1,2494 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Management.Automation.Subsystem; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; + +using Microsoft.PowerShell.Commands; + +namespace Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform +{ + /// + /// Class that defines Dsc cache entries. + /// + internal class DscClassCacheEntry + { + /// + /// Initializes a new instance of the class. + /// + public DscClassCacheEntry() + : this(DSCResourceRunAsCredential.Default, isImportedImplicitly: false, cimClassInstance: null, modulePath: string.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Run as credential value. + /// Resource is imported implicitly. + /// Class definition. + /// Path of module defining the class. + public DscClassCacheEntry(DSCResourceRunAsCredential dscResourceRunAsCredential, bool isImportedImplicitly, PSObject cimClassInstance, string modulePath) + { + DscResRunAsCred = dscResourceRunAsCredential; + IsImportedImplicitly = isImportedImplicitly; + CimClassInstance = cimClassInstance; + ModulePath = modulePath; + } + + /// + /// Gets or sets the RunAs Credentials that this DSC resource will use. + /// + public DSCResourceRunAsCredential DscResRunAsCred { get; set; } + + /// + /// Gets or sets a value indicating if we have implicitly imported this resource. + /// + public bool IsImportedImplicitly { get; set; } + + /// + /// Gets or sets CimClass instance for this resource. + /// + public PSObject CimClassInstance { get; set; } + + /// + /// Gets or sets path of the implementing module for this resource. + /// + public string ModulePath { get; set; } + } + + /// + /// DSC class cache for this runspace. + /// + [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes", + Justification = "Needed Internal use only")] + public static class DscClassCache + { + private static readonly HashSet s_reservedDynamicKeywords = new HashSet(new[] { "Synchronization", "Certificate", "IIS", "SQL" }, StringComparer.OrdinalIgnoreCase); + + private static readonly HashSet s_reservedProperties = new HashSet(new[] { "Require", "Trigger", "Notify", "Before", "After", "Subscribe" }, StringComparer.OrdinalIgnoreCase); + + private static readonly PSTraceSource s_tracer = PSTraceSource.GetTracer("DSC", "DSC Class Cache"); + + // Constants for items in the module qualified name (Module\Version\ClassName) + private const int ModuleNameIndex = 0; + private const int ModuleVersionIndex = 1; + private const int ClassNameIndex = 2; + private const int FriendlyNameIndex = 3; + + // Create a HashSet for fast lookup. According to MSDN, the time complexity of search for an element in a HashSet is O(1) + private static readonly HashSet s_hiddenResourceCache = + new HashSet(StringComparer.OrdinalIgnoreCase) { "MSFT_BaseConfigurationProviderRegistration", "MSFT_CimConfigurationProviderRegistration", "MSFT_PSConfigurationProviderRegistration" }; + + // A collection to prevent circular importing case when Import-DscResource does not have a module specified + [ThreadStatic] + private static readonly HashSet t_currentImportDscResourceInvocations = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets DSC class cache for this runspace. + /// Cache stores the DSCRunAsBehavior, cim class and boolean to indicate if an Inbox resource has been implicitly imported. + /// + private static Dictionary ClassCache + { + get => t_classCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + [ThreadStatic] + private static Dictionary t_classCache; + + /// + /// Gets DSC class cache for GuestConfig; it is similar to ClassCache, but maintains values between operations. + /// + private static Dictionary GuestConfigClassCache + { + get => t_guestConfigClassCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + [ThreadStatic] + private static Dictionary t_guestConfigClassCache; + + /// + /// DSC classname to source module mapper. + /// + private static Dictionary> ByClassModuleCache + => t_byClassModuleCache ??= new Dictionary>(StringComparer.OrdinalIgnoreCase); + + [ThreadStatic] + private static Dictionary> t_byClassModuleCache; + + /// + /// Default ModuleName and ModuleVersion to use. + /// + private static readonly Tuple s_defaultModuleInfoForResource = new Tuple("PSDesiredStateConfiguration", new Version(3, 0)); + + /// + /// When this property is set to true, DSC Cache will cache multiple versions of a resource. + /// That means it will cache duplicate resource classes (class names for a resource in two different module versions are same). + /// NOTE: This property should be set to false for DSC compiler related methods/functionality, such as Import-DscResource, + /// because the Mof serializer does not support deserialization of classes with different versions. + /// + [ThreadStatic] + private static bool t_cacheResourcesFromMultipleModuleVersions; + + private static bool CacheResourcesFromMultipleModuleVersions + { + get + { + return t_cacheResourcesFromMultipleModuleVersions; + } + + set + { + t_cacheResourcesFromMultipleModuleVersions = value; + } + } + + [ThreadStatic] + private static bool t_newApiIsUsed = false; + + /// + /// Flag shows if PS7 DSC APIs were used. + /// + public static bool NewApiIsUsed + { + get + { + return t_newApiIsUsed; + } + + set + { + t_newApiIsUsed = value; + } + } + + /// + /// Initialize the class cache with the default classes in $ENV:SystemDirectory\Configuration. + /// + public static void Initialize() + { + Initialize(errors: null, modulePathList: null); + } + + /// + /// Initialize the class cache with default classes that come with PSDesiredStateConfiguration module. + /// + /// Collection of any errors encountered during initialization. + /// List of module path from where DSC PS modules will be loaded. + public static void Initialize(Collection errors, List modulePathList) + { + s_tracer.WriteLine("Initializing DSC class cache"); + + // Load the base schema files. + ClearCache(); + var dscConfigurationDirectory = Environment.GetEnvironmentVariable("DSC_HOME"); + if (string.IsNullOrEmpty(dscConfigurationDirectory)) + { + var moduleInfos = ModuleCmdletBase.GetModuleIfAvailable(new Microsoft.PowerShell.Commands.ModuleSpecification() + { + Name = "PSDesiredStateConfiguration", + + // Version in the next line is actually MinimumVersion + Version = new Version(3, 0, 0) + }); + + if (moduleInfos.Count > 0) + { + // to be consistent with Import-Module behavior, we use the first occurrence that we find in PSModulePath + var moduleDirectory = Path.GetDirectoryName(moduleInfos[0].Path); + dscConfigurationDirectory = Path.Join(moduleDirectory, "Configuration"); + } + else + { + // when all else has failed use location of system-wide PS module directory (i.e. /usr/local/share/powershell/Modules) as backup + dscConfigurationDirectory = Path.Join(ModuleIntrinsics.GetSharedModulePath(), "PSDesiredStateConfiguration", "Configuration"); + } + } + + if (!Directory.Exists(dscConfigurationDirectory)) + { + throw new DirectoryNotFoundException(string.Format(ParserStrings.PsDscMissingSchemaStore, dscConfigurationDirectory)); + } + + var resourceBaseFile = Path.Join(dscConfigurationDirectory, "BaseRegistration", "BaseResource.schema.json"); + ImportBaseClasses(resourceBaseFile, s_defaultModuleInfoForResource, errors, false); + var metaConfigFile = Path.Join(dscConfigurationDirectory, "BaseRegistration", "MSFT_DSCMetaConfiguration.json"); + ImportBaseClasses(metaConfigFile, s_defaultModuleInfoForResource, errors, false); + } + + /// + /// Import base classes from the given file. + /// + /// Path to schema file. + /// Module information. + /// Error collection that will be shown to the user. + /// Flag for implicitly imported resource. + /// Class objects from schema file. + public static IEnumerable ImportBaseClasses(string path, Tuple moduleInfo, Collection errors, bool importInBoxResourcesImplicitly) + { + if (string.IsNullOrEmpty(path)) + { + throw PSTraceSource.NewArgumentNullException(nameof(path)); + } + + s_tracer.WriteLine("DSC ClassCache: importing file: {0}", path); + + var parser = new CimDSCParser(); + + IEnumerable classes = null; + try + { + classes = parser.ParseSchemaJson(path); + } + catch (PSInvalidOperationException e) + { + // Ignore modules with invalid schemas. + s_tracer.WriteLine("DSC ClassCache: Error importing file '{0}', with error '{1}'. Skipping file.", path, e); + if (errors != null) + { + errors.Add(e); + } + } + + if (classes != null) + { + foreach (dynamic c in classes) + { + var className = c.ClassName; + + if (string.IsNullOrEmpty(className)) + { + // ClassName is empty - skipping class import + continue; + } + + string alias = GetFriendlyName(c); + var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; + string moduleQualifiedResourceName = GetModuleQualifiedResourceName(moduleInfo.Item1, moduleInfo.Item2.ToString(), className, friendlyName); + DscClassCacheEntry cimClassInfo; + + if (ClassCache.TryGetValue(moduleQualifiedResourceName, out cimClassInfo)) + { + if (errors != null) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException( + ParserStrings.DuplicateCimClassDefinition, className, path, cimClassInfo.ModulePath); + + e.SetErrorId("DuplicateCimClassDefinition"); + errors.Add(e); + } + + continue; + } + + if (s_hiddenResourceCache.Contains(className)) + { + continue; + } + + var classCacheEntry = new DscClassCacheEntry(DSCResourceRunAsCredential.NotSupported, importInBoxResourcesImplicitly, c, path); + ClassCache[moduleQualifiedResourceName] = classCacheEntry; + GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; + ByClassModuleCache[className] = moduleInfo; + } + + var sb = new System.Text.StringBuilder(); + foreach (dynamic c in classes) + { + sb.Append(c.ClassName); + sb.Append(','); + } + + s_tracer.WriteLine("DSC ClassCache: loading file '{0}' added the following classes to the cache: {1}", path, sb.ToString()); + } + else + { + s_tracer.WriteLine("DSC ClassCache: loading file '{0}' added no classes to the cache."); + } + + return classes; + } + + /// + /// Get text from SecureString. + /// + /// Value of SecureString. + /// Decoded string. + public static string GetStringFromSecureString(SecureString value) + { + string passwordValueToAdd = string.Empty; + + if (value != null) + { + IntPtr ptr = Marshal.SecureStringToCoTaskMemUnicode(value); + passwordValueToAdd = Marshal.PtrToStringUni(ptr); + Marshal.ZeroFreeCoTaskMemUnicode(ptr); + } + + return passwordValueToAdd; + } + + /// + /// Clear out the existing collection of CIM classes and associated keywords. + /// + public static void ClearCache() + { + if (!ExperimentalFeature.IsEnabled(ICrossPlatformDsc.DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + s_tracer.WriteLine("DSC class: clearing the cache and associated keywords."); + ClassCache.Clear(); + ByClassModuleCache.Clear(); + CacheResourcesFromMultipleModuleVersions = false; + t_currentImportDscResourceInvocations.Clear(); + } + + private static string GetModuleQualifiedResourceName(string moduleName, string moduleVersion, string className, string resourceName) + { + return string.Format(CultureInfo.InvariantCulture, "{0}\\{1}\\{2}\\{3}", moduleName, moduleVersion, className, resourceName); + } + + private static List> FindResourceInCache(string moduleName, string className, string resourceName) + { + return (from cacheEntry in ClassCache + let splittedName = cacheEntry.Key.Split(Utils.Separators.Backslash) + let cachedClassName = splittedName[ClassNameIndex] + let cachedModuleName = splittedName[ModuleNameIndex] + let cachedResourceName = splittedName[FriendlyNameIndex] + where (string.Equals(cachedResourceName, resourceName, StringComparison.OrdinalIgnoreCase) + || (string.Equals(cachedClassName, className, StringComparison.OrdinalIgnoreCase) + && string.Equals(cachedModuleName, moduleName, StringComparison.OrdinalIgnoreCase))) + select cacheEntry).ToList(); + } + + /// + /// Returns class declaration from GuestConfigClassCache. + /// + /// Module name. + /// Module version. + /// Name of the class. + /// Friendly name of the resource. + /// Class declaration from cache. + public static PSObject GetGuestConfigCachedClass(string moduleName, string moduleVersion, string className, string resourceName) + { + if (!ExperimentalFeature.IsEnabled(ICrossPlatformDsc.DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + var moduleQualifiedResourceName = GetModuleQualifiedResourceName(moduleName, moduleVersion, className, string.IsNullOrEmpty(resourceName) ? className : resourceName); + DscClassCacheEntry classCacheEntry = null; + if (GuestConfigClassCache.TryGetValue(moduleQualifiedResourceName, out classCacheEntry)) + { + return classCacheEntry.CimClassInstance; + } + else + { + // if class was not found with current ResourceName then it may be a class with non-empty FriendlyName that caller does not know, so perform a broad search + string partialClassPath = string.Join('\\', moduleName, moduleVersion, className, string.Empty); + foreach (string key in GuestConfigClassCache.Keys) + { + if (key.StartsWith(partialClassPath)) + { + return GuestConfigClassCache[key].CimClassInstance; + } + } + + return null; + } + } + + /// + /// Clears GuestConfigClassCache. + /// + public static void ClearGuestConfigClassCache() + { + GuestConfigClassCache.Clear(); + } + + private static bool IsMagicProperty(string propertyName) + { + return System.Text.RegularExpressions.Regex.Match(propertyName, "^(ResourceId|SourceInfo|ModuleName|ModuleVersion|ConfigurationName)$", System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success; + } + + private static string GetFriendlyName(dynamic cimClass) + { + return cimClass.FriendlyName; + } + + /// + /// Method to get the cached classes in the form of DynamicKeyword. + /// + /// Dynamic keyword collection. + public static Collection GetKeywordsFromCachedClasses() + { + if (!ExperimentalFeature.IsEnabled(ICrossPlatformDsc.DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + Collection keywords = new Collection(); + + foreach (KeyValuePair cachedClass in ClassCache) + { + string[] splittedName = cachedClass.Key.Split(Utils.Separators.Backslash); + string moduleName = splittedName[ModuleNameIndex]; + string moduleVersion = splittedName[ModuleVersionIndex]; + + var keyword = CreateKeywordFromCimClass(moduleName, Version.Parse(moduleVersion), cachedClass.Value.CimClassInstance, cachedClass.Value.DscResRunAsCred); + if (keyword is not null) + { + keywords.Add(keyword); + } + } + + return keywords; + } + + private static void CreateAndRegisterKeywordFromCimClass(string moduleName, Version moduleVersion, PSObject cimClass, Dictionary functionsToDefine, DSCResourceRunAsCredential runAsBehavior) + { + var keyword = CreateKeywordFromCimClass(moduleName, moduleVersion, cimClass, runAsBehavior); + if (keyword is null) + { + return; + } + + // keyword is already defined and we don't allow redefine it + if (!CacheResourcesFromMultipleModuleVersions && DynamicKeyword.ContainsKeyword(keyword.Keyword)) + { + var oldKeyword = DynamicKeyword.GetKeyword(keyword.Keyword); + if (oldKeyword.ImplementingModule is null || + !oldKeyword.ImplementingModule.Equals(moduleName, StringComparison.OrdinalIgnoreCase) || oldKeyword.ImplementingModuleVersion != moduleVersion) + { + var e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateKeywordDefinition, keyword.Keyword); + e.SetErrorId("DuplicateKeywordDefinition"); + throw e; + } + } + + // Add the dynamic keyword to the table + DynamicKeyword.AddKeyword(keyword); + + // And now define the driver functions in the current scope... + if (functionsToDefine != null) + { + functionsToDefine[moduleName + "\\" + keyword.Keyword] = CimKeywordImplementationFunction; + } + } + + private static DynamicKeyword CreateKeywordFromCimClass(string moduleName, Version moduleVersion, dynamic cimClass, DSCResourceRunAsCredential runAsBehavior) + { + var resourceName = cimClass.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 (s_reservedDynamicKeywords.Contains(keywordString)) + { + keyword.IsReservedKeyword = true; + } + + // see if it's a resource type i.e. it inherits from OMI_BaseResource + bool isResourceType = false; + + // previous version of this code was the only place that referenced CimSuperClass + // so to simplify things we just check superclass to be OMI_BaseResource + // with assumption that current code will not work for multi-level class inheritance (which is never used in practice according to DSC team) + // this simplification allows us to avoid linking objects together using CimSuperClass field during deserialization + if ((!string.IsNullOrEmpty(cimClass.SuperClassName)) && string.Equals("OMI_BaseResource", cimClass.SuperClassName, StringComparison.OrdinalIgnoreCase)) + { + isResourceType = true; + } + + // 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 + if (cimClass.ClassProperties != null) + { + foreach (var prop in cimClass.ClassProperties) + { + // If the property has the Read qualifier, skip it. + if (string.Equals(prop.Qualifiers?.Read?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) + { + 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 (s_reservedProperties.Contains(prop.Name)) + { + keyword.HasReservedProperties = true; + continue; + } + + // Otherwise, add it to the Keyword List. + var keyProp = new System.Management.Automation.Language.DynamicKeywordProperty(); + keyProp.Name = prop.Name; + + // 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 == "Instance" && !referenceClassNameIsNullOrEmpty) + { + keyProp.TypeConstraint = prop.ReferenceClassName; + } + else if (prop.CimType == "InstanceArray" && !referenceClassNameIsNullOrEmpty) + { + keyProp.TypeConstraint = prop.ReferenceClassName + "[]"; + } + else + { + keyProp.TypeConstraint = prop.CimType.ToString(); + } + + // Check to see if there is a Values attribute and save the list of allowed values if so. + var values = prop.Qualifiers?.Values; + if (values is not null) + { + foreach (var val in values) + { + keyProp.Values.Add(val.ToString()); + } + } + + // Check to see if there is a ValueMap attribute and save the list of allowed values if so. + var nativeValueMap = prop.Qualifiers?.ValueMap; + List valueMap = null; + if (nativeValueMap is not null) + { + valueMap = new List(); + foreach (var val in nativeValueMap) + { + valueMap.Add(val.ToString()); + } + } + + // Check to see if this property has the Required qualifier associated with it. + if (string.Equals(prop.Qualifiers?.Required?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) + { + keyProp.Mandatory = true; + } + + // Check to see if this property has the Key qualifier associated with it. + if (string.Equals(prop.Qualifiers?.Key?.ToString(), "True", StringComparison.OrdinalIgnoreCase)) + { + keyProp.Mandatory = true; + keyProp.IsKey = 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 is not null && keyProp.Values.Count > 0) + { + if (valueMap.Count != 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.Count, + keyword.Keyword); + return null; + } + + for (int index = 0; index < valueMap.Count; 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; + } + + private static void UpdateKnownRestriction(DynamicKeyword keyword) + { + const int RefreshFrequencyMin = 30; + const int RefreshFrequencyMax = 44640; + + const int ConfigurationModeFrequencyMin = 15; + const int ConfigurationModeFrequencyMax = 44640; + + if ( + string.Equals( + keyword.ResourceName, + "MSFT_DSCMetaConfigurationV2", + StringComparison.OrdinalIgnoreCase) + || + string.Equals( + keyword.ResourceName, + "MSFT_DSCMetaConfiguration", + StringComparison.OrdinalIgnoreCase)) + { + if (keyword.Properties["RefreshFrequencyMins"] is not null) + { + keyword.Properties["RefreshFrequencyMins"].Range = new Tuple(RefreshFrequencyMin, RefreshFrequencyMax); + } + + if (keyword.Properties["ConfigurationModeFrequencyMins"] != null) + { + keyword.Properties["ConfigurationModeFrequencyMins"].Range = new Tuple(ConfigurationModeFrequencyMin, ConfigurationModeFrequencyMax); + } + + if (keyword.Properties["DebugMode"] is not null) + { + keyword.Properties["DebugMode"].Values.Remove("ResourceScriptBreakAll"); + keyword.Properties["DebugMode"].ValueMap.Remove("ResourceScriptBreakAll"); + } + } + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// Collection of any errors encountered while loading keywords. + public static void LoadDefaultCimKeywords(Collection errors) + { + LoadDefaultCimKeywords(functionsToDefine: null, errors, modulePathList: null, cacheResourcesFromMultipleModuleVersions: false); + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// A dictionary to add the defined functions to, may be null. + public static void LoadDefaultCimKeywords(Dictionary functionsToDefine) + { + LoadDefaultCimKeywords(functionsToDefine, errors: null, modulePathList: null, cacheResourcesFromMultipleModuleVersions: false); + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// Collection of any errors encountered while loading keywords. + /// Allow caching the resources from multiple versions of modules. + public static void LoadDefaultCimKeywords(Collection errors, bool cacheResourcesFromMultipleModuleVersions) + { + LoadDefaultCimKeywords(functionsToDefine: null, errors, modulePathList: null, cacheResourcesFromMultipleModuleVersions); + } + + /// + /// Load the default system CIM classes and create the corresponding keywords. + /// + /// A dictionary to add the defined functions to, may be null. + /// Collection of any errors encountered while loading keywords. + /// List of module path from where DSC PS modules will be loaded. + /// Allow caching the resources from multiple versions of modules. + private static void LoadDefaultCimKeywords( + Dictionary functionsToDefine, + Collection errors, + List modulePathList, + bool cacheResourcesFromMultipleModuleVersions) + { + if (!ExperimentalFeature.IsEnabled(ICrossPlatformDsc.DscExperimentalFeatureName)) + { + Exception exception = new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + errors.Add(exception); + return; + } + + NewApiIsUsed = true; + 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 (dynamic cimClass in ClassCache.Values) + { + var className = cimClass.CimClassInstance.ClassName; + var moduleInfo = ByClassModuleCache[className]; + CreateAndRegisterKeywordFromCimClass(moduleInfo.Item1, moduleInfo.Item2, cimClass.CimClassInstance, functionsToDefine, cimClass.DscResRunAsCred); + } + + // 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); + } + } + + // This function is called after parsing the Import-DscResource keyword and it's arguments, but before parsing anything else. + private static ParseError[] ImportResourcePostParse(DynamicKeywordStatementAst ast) + { + var elements = Ast.CopyElements(ast.CommandElements); + var commandAst = new CommandAst(ast.Extent, elements, TokenKind.Unknown, null); + + const string NameParam = "Name"; + const string ModuleNameParam = "ModuleName"; + const string ModuleVersionParam = "ModuleVersion"; + + StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, false); + + var errorList = new List(); + foreach (var bindingException in bindingResult.BindingExceptions.Values) + { + errorList.Add(new ParseError(bindingException.CommandElement.Extent, "ParameterBindingException", bindingException.BindingException.Message)); + } + + ParameterBindingResult moduleNameBindingResult = null; + ParameterBindingResult resourceNameBindingResult = null; + ParameterBindingResult moduleVersionBindingResult = null; + + foreach (var binding in bindingResult.BoundParameters) + { + // Error case when positional parameter values are specified + var boundParameterName = binding.Key; + var parameterBindingResult = binding.Value; + if (boundParameterName.All(char.IsDigit)) + { + errorList.Add(new ParseError(parameterBindingResult.Value.Extent, "ImportDscResourcePositionalParamsNotSupported", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourcePositionalParamsNotSupported))); + continue; + } + + if (NameParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) + { + resourceNameBindingResult = parameterBindingResult; + } + else if (ModuleNameParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) + { + moduleNameBindingResult = parameterBindingResult; + } + else if (ModuleVersionParam.StartsWith(boundParameterName, StringComparison.OrdinalIgnoreCase)) + { + moduleVersionBindingResult = parameterBindingResult; + } + else + { + errorList.Add(new ParseError(parameterBindingResult.Value.Extent, "ImportDscResourceNeedParams", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + } + + if (errorList.Count == 0 && moduleNameBindingResult == null && resourceNameBindingResult == null) + { + errorList.Add(new ParseError(ast.Extent, "ImportDscResourceNeedParams", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + + // Check here if Version is specified but modulename is not specified + if (moduleVersionBindingResult != null && moduleNameBindingResult == null) + { + // only add this error again to the error list if resources is not null + // if resources and modules are both null we have already added this error in collection + // we do not want to do this twice. since we are giving same error ImportDscResourceNeedParams in both cases + // once we have different error messages for 2 scenarios we can remove this check + if (resourceNameBindingResult is not null) + { + errorList.Add(new ParseError(ast.Extent, "ImportDscResourceNeedModuleNameWithModuleVersion", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + } + + string[] resourceNames = null; + if (resourceNameBindingResult is not null) + { + object resourceName = null; + if (!IsConstantValueVisitor.IsConstant(resourceNameBindingResult.Value, out resourceName, true, true) || + !LanguagePrimitives.TryConvertTo(resourceName, out resourceNames)) + { + errorList.Add(new ParseError(resourceNameBindingResult.Value.Extent, "RequiresInvalidStringArgument", string.Format(CultureInfo.CurrentCulture, ParserStrings.RequiresInvalidStringArgument, NameParam))); + } + } + + System.Version moduleVersion = null; + if (moduleVersionBindingResult is not null) + { + object moduleVer = null; + if (!IsConstantValueVisitor.IsConstant(moduleVersionBindingResult.Value, out moduleVer, true, true)) + { + errorList.Add(new ParseError(moduleVersionBindingResult.Value.Extent, "RequiresArgumentMustBeConstant", ParserStrings.RequiresArgumentMustBeConstant)); + } + + if (moduleVer is double) + { + // this happens in case -ModuleVersion 1.0, then use extent text for that. + // The better way to do it would be define static binding API against CommandInfo, that holds information about parameter types. + // This way, we can avoid this ugly special-casing and say that -ModuleVersion has type [System.Version]. + moduleVer = moduleVersionBindingResult.Value.Extent.Text; + } + + if (!LanguagePrimitives.TryConvertTo(moduleVer, out moduleVersion)) + { + errorList.Add(new ParseError(moduleVersionBindingResult.Value.Extent, "RequiresVersionInvalid", ParserStrings.RequiresVersionInvalid)); + } + } + + ModuleSpecification[] moduleSpecifications = null; + if (moduleNameBindingResult is not null) + { + object moduleName = null; + if (!IsConstantValueVisitor.IsConstant(moduleNameBindingResult.Value, out moduleName, true, true)) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "RequiresArgumentMustBeConstant", ParserStrings.RequiresArgumentMustBeConstant)); + } + + if (LanguagePrimitives.TryConvertTo(moduleName, out moduleSpecifications)) + { + // if resourceNames are specified then we can not specify multiple modules name + if (moduleSpecifications is not null && moduleSpecifications.Length > 1 && resourceNames is not null) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModulesNotSupportedWithName", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceMultipleModulesNotSupportedWithName))); + } + + // if moduleversion is specified then we can not specify multiple modules name + if (moduleSpecifications is not null && moduleSpecifications.Length > 1 && moduleVersion is not null) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModulesNotSupportedWithVersion", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + + // if moduleversion is specified then we can not specify another version in modulespecification object of ModuleName + if (moduleSpecifications is not null && (moduleSpecifications[0].Version is not null || moduleSpecifications[0].MaximumVersion is not null) && moduleVersion is not null) + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "ImportDscResourceMultipleModuleVersionsNotSupported", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceNeedParams))); + } + + // If moduleVersion is specified we have only one module Name in valid scenario + // So update it's version property in module specification object that will be used to load modules + if (moduleSpecifications is not null && moduleSpecifications[0].Version is null && moduleSpecifications[0].MaximumVersion is null && moduleVersion is not null) + { + moduleSpecifications[0].Version = moduleVersion; + } + } + else + { + errorList.Add(new ParseError(moduleNameBindingResult.Value.Extent, "RequiresInvalidStringArgument", string.Format(CultureInfo.CurrentCulture, ParserStrings.RequiresInvalidStringArgument, ModuleNameParam))); + } + } + + if (errorList.Count == 0) + { + // No errors, try to load the resources + LoadResourcesFromModuleInImportResourcePostParse(ast.Extent, moduleSpecifications, resourceNames, errorList); + } + + return errorList.ToArray(); + } + + // This function performs semantic checks for Import-DscResource + private static ParseError[] ImportResourceCheckSemantics(DynamicKeywordStatementAst ast) + { + List errorList = null; + + var keywordAst = Ast.GetAncestorAst(ast.Parent); + while (keywordAst is not null) + { + if (keywordAst.Keyword.Keyword.Equals("Node")) + { + if (errorList is null) + { + errorList = new List(); + } + + errorList.Add(new ParseError(ast.Extent, "ImportDscResourceInsideNode", string.Format(CultureInfo.CurrentCulture, ParserStrings.ImportDscResourceInsideNode))); + break; + } + + keywordAst = Ast.GetAncestorAst(keywordAst.Parent); + } + + if (errorList is not null) + { + return errorList.ToArray(); + } + else + { + return null; + } + } + + // This function performs semantic checks for all DSC Resources keywords. + private static ParseError[] CheckMandatoryPropertiesPresent(DynamicKeywordStatementAst ast) + { + HashSet mandatoryPropertiesNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var pair in ast.Keyword.Properties) + { + if (pair.Value.Mandatory) + { + mandatoryPropertiesNames.Add(pair.Key); + } + } + + // by design mandatoryPropertiesNames are not empty at this point: + // every resource must have at least one Key property. + HashtableAst hashtableAst = null; + foreach (var commandElementsAst in ast.CommandElements) + { + hashtableAst = commandElementsAst as HashtableAst; + if (hashtableAst != null) + { + break; + } + } + + if (hashtableAst is null) + { + // nothing to validate + return null; + } + + foreach (var pair in hashtableAst.KeyValuePairs) + { + object evalResultObject; + if (IsConstantValueVisitor.IsConstant(pair.Item1, out evalResultObject, forAttribute: false, forRequires: false)) + { + var presentName = evalResultObject as string; + if (presentName is not null) + { + if (mandatoryPropertiesNames.Remove(presentName) && mandatoryPropertiesNames.Count == 0) + { + // optimization, once all mandatory properties are specified, we can safely exit. + return null; + } + } + } + } + + if (mandatoryPropertiesNames.Count > 0) + { + ParseError[] errors = new ParseError[mandatoryPropertiesNames.Count]; + var extent = ast.CommandElements[0].Extent; + int i = 0; + foreach (string name in mandatoryPropertiesNames) + { + errors[i] = new ParseError( + extent, + "MissingValueForMandatoryProperty", + string.Format( + CultureInfo.CurrentCulture, + ParserStrings.MissingValueForMandatoryProperty, + ast.Keyword.Keyword, + ast.Keyword.Properties.First(p => StringComparer.OrdinalIgnoreCase.Equals(p.Value.Name, name)).Value.TypeConstraint, + name)); + i++; + } + + return errors; + } + + return null; + } + + /// + /// Load DSC resources from specified module. + /// + /// Script statement loading the module, can be null. + /// Module information, can be null. + /// Name of the resource to be loaded from module. + /// List of errors reported by the method. + internal static void LoadResourcesFromModuleInImportResourcePostParse( + IScriptExtent scriptExtent, + ModuleSpecification[] moduleSpecifications, + string[] resourceNames, + List errorList) + { + // get all required modules + var modules = new Collection(); + if (moduleSpecifications is not null) + { + foreach (var moduleToImport in moduleSpecifications) + { + bool foundModule = false; + var moduleInfos = ModuleCmdletBase.GetModuleIfAvailable(moduleToImport); + + if (moduleInfos.Count >= 1 && (moduleToImport.Version is not null || moduleToImport.Guid is not null)) + { + foreach (var psModuleInfo in moduleInfos) + { + if ((moduleToImport.Guid.HasValue && moduleToImport.Guid.Equals(psModuleInfo.Guid)) || + (moduleToImport.Version is not null && + moduleToImport.Version.Equals(psModuleInfo.Version))) + { + modules.Add(psModuleInfo); + foundModule = true; + break; + } + } + } + else if (moduleInfos.Count == 1) + { + modules.Add(moduleInfos[0]); + foundModule = true; + } + + if (!foundModule) + { + if (moduleInfos.Count > 1) + { + errorList.Add( + new ParseError( + scriptExtent, + "MultipleModuleEntriesFoundDuringParse", + string.Format(CultureInfo.CurrentCulture, ParserStrings.MultipleModuleEntriesFoundDuringParse, moduleToImport.Name))); + } + else + { + string moduleString = moduleToImport.Version == null + ? moduleToImport.Name + : string.Format(CultureInfo.CurrentCulture, "<{0}, {1}>", moduleToImport.Name, moduleToImport.Version); + + errorList.Add(new ParseError(scriptExtent, "ModuleNotFoundDuringParse", string.Format(CultureInfo.CurrentCulture, ParserStrings.ModuleNotFoundDuringParse, moduleString))); + } + + return; + } + } + } + else if (resourceNames is not null) + { + // Lookup the required resources under available PowerShell modules when modulename is not specified + // Make sure that this is not a circular import/parsing + var callLocation = string.Join(':', scriptExtent.File, scriptExtent.StartLineNumber, scriptExtent.StartColumnNumber, scriptExtent.Text); + if (!t_currentImportDscResourceInvocations.Contains(callLocation)) + { + t_currentImportDscResourceInvocations.Add(callLocation); + using (var powerShell = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + powerShell.AddCommand("Get-Module"); + powerShell.AddParameter("ListAvailable"); + modules = powerShell.Invoke(); + } + } + } + + // When ModuleName only specified, we need to import all resources from that module + var resourcesToImport = new List(); + if (resourceNames is null || resourceNames.Length == 0) + { + resourcesToImport.Add("*"); + } + else + { + resourcesToImport.AddRange(resourceNames); + } + + foreach (var moduleInfo in modules) + { + var resourcesFound = new List(); + var exceptionList = new System.Collections.ObjectModel.Collection(); + LoadPowerShellClassResourcesFromModule(primaryModuleInfo: moduleInfo, moduleInfo: moduleInfo, resourcesToImport: resourcesToImport, resourcesFound: resourcesFound, errorList: exceptionList, functionsToDefine: null, recurse: true, extent: scriptExtent); + foreach (Exception ex in exceptionList) + { + errorList.Add(new ParseError(scriptExtent, "ClassResourcesLoadingFailed", ex.Message)); + } + + foreach (var resource in resourcesFound) + { + resourcesToImport.Remove(resource); + } + + if (resourcesToImport.Count == 0) + { + break; + } + } + + if (resourcesToImport.Count > 0) + { + foreach (var resourceNameToImport in resourcesToImport) + { + if (!resourceNameToImport.Contains('*')) + { + errorList.Add(new ParseError(scriptExtent, "DscResourcesNotFoundDuringParsing", string.Format(CultureInfo.CurrentCulture, ParserStrings.DscResourcesNotFoundDuringParsing, resourceNameToImport))); + } + } + } + } + + private static void LoadPowerShellClassResourcesFromModule( + PSModuleInfo primaryModuleInfo, + PSModuleInfo moduleInfo, + ICollection resourcesToImport, + ICollection resourcesFound, + Collection errorList, + Dictionary functionsToDefine = null, + bool recurse = true, + IScriptExtent extent = null) + { + if (primaryModuleInfo._declaredDscResourceExports is null || primaryModuleInfo._declaredDscResourceExports.Count == 0) + { + return; + } + + if (moduleInfo.ModuleType == ModuleType.Binary) + { + throw PSTraceSource.NewArgumentException("isConfiguration", ParserStrings.ConfigurationNotSupportedInPowerShellCore); + } + else + { + string scriptPath = null; + if (moduleInfo.RootModule is not null) + { + scriptPath = Path.Join(moduleInfo.ModuleBase, moduleInfo.RootModule); + } + else if (moduleInfo.Path is not null) + { + scriptPath = moduleInfo.Path; + } + + LoadPowerShellClassResourcesFromModule(scriptPath, primaryModuleInfo, resourcesToImport, resourcesFound, functionsToDefine, errorList, extent); + } + + if (moduleInfo.NestedModules is not null && recurse) + { + foreach (var nestedModule in moduleInfo.NestedModules) + { + LoadPowerShellClassResourcesFromModule(primaryModuleInfo, nestedModule, resourcesToImport, resourcesFound, errorList, functionsToDefine, recurse: false, extent: extent); + } + } + } + + /// + /// Import class resources from module. + /// + /// Module information. + /// Collection of resources to import. + /// Functions to define. + /// List of errors to return. + /// The list of resources imported from this module. + public static List ImportClassResourcesFromModule(PSModuleInfo moduleInfo, ICollection resourcesToImport, Dictionary functionsToDefine, Collection errors) + { + if (!ExperimentalFeature.IsEnabled(ICrossPlatformDsc.DscExperimentalFeatureName)) + { + throw new InvalidOperationException(ParserStrings.PS7DscSupportDisabled); + } + + var resourcesImported = new List(); + LoadPowerShellClassResourcesFromModule(moduleInfo, moduleInfo, resourcesToImport, resourcesImported, errors, functionsToDefine); + return resourcesImported; + } + + internal static PSObject[] GenerateJsonClassesForAst(TypeDefinitionAst typeAst, PSModuleInfo module, DSCResourceRunAsCredential runAsBehavior) + { + var embeddedInstanceTypes = new List(); + + var result = GenerateJsonClassesForAst(typeAst, embeddedInstanceTypes); + var visitedInstances = new List(); + visitedInstances.Add(typeAst); + var classes = ProcessEmbeddedInstanceTypes(embeddedInstanceTypes, visitedInstances); + AddEmbeddedInstanceTypesToCaches(classes, module, runAsBehavior); + + return result; + } + + private static List ProcessEmbeddedInstanceTypes(List embeddedInstanceTypes, List visitedInstances) + { + var result = new List(); + while (embeddedInstanceTypes.Count > 0) + { + var batchedTypes = embeddedInstanceTypes.Where(x => !visitedInstances.Contains(x)).ToArray(); + embeddedInstanceTypes.Clear(); + + for (int i = batchedTypes.Length - 1; i >= 0; i--) + { + visitedInstances.Add(batchedTypes[i]); + var typeAst = batchedTypes[i] as TypeDefinitionAst; + if (typeAst is not null) + { + var classes = GenerateJsonClassesForAst(typeAst, embeddedInstanceTypes); + result.AddRange(classes); + } + } + } + + return result; + } + + private static void AddEmbeddedInstanceTypesToCaches(IEnumerable classes, PSModuleInfo module, DSCResourceRunAsCredential runAsBehavior) + { + foreach (dynamic c in classes) + { + var className = c.ClassName; + string alias = GetFriendlyName(c); + var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; + var moduleQualifiedResourceName = GetModuleQualifiedResourceName(module.Name, module.Version.ToString(), className, friendlyName); + var classCacheEntry = new DscClassCacheEntry(runAsBehavior, false, c, module.Path); + ClassCache[moduleQualifiedResourceName] = classCacheEntry; + GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; + ByClassModuleCache[className] = new Tuple(module.Name, module.Version); + } + } + + internal static string MapTypeNameToMofType(ITypeName typeName, string memberName, string className, out bool isArrayType, out string embeddedInstanceType, List embeddedInstanceTypes, ref string[] enumNames) + { + TypeName propTypeName; + var arrayTypeName = typeName as ArrayTypeName; + if (arrayTypeName is not null) + { + isArrayType = true; + propTypeName = arrayTypeName.ElementType as TypeName; + } + else + { + isArrayType = false; + propTypeName = typeName as TypeName; + } + + if (propTypeName is null || propTypeName._typeDefinitionAst is null) + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + ParserStrings.UnsupportedPropertyTypeOfDSCResourceClass, + memberName, + typeName.FullName, + typeName)); + } + + if (propTypeName._typeDefinitionAst.IsEnum) + { + enumNames = propTypeName._typeDefinitionAst.Members.Select(m => m.Name).ToArray(); + isArrayType = false; + embeddedInstanceType = null; + return "string"; + } + + if (!embeddedInstanceTypes.Contains(propTypeName._typeDefinitionAst)) + { + embeddedInstanceTypes.Add(propTypeName._typeDefinitionAst); + } + + embeddedInstanceType = propTypeName.Name.Replace('.', '_'); + return "Instance"; + } + + private static PSObject[] GenerateJsonClassesForAst(TypeDefinitionAst typeAst, List embeddedInstanceTypes) + { + // MOF-based implementation of this used to generate MOF string representing classes/typeAst and pass it to MMI/MOF deserializer to get CimClass array + // Here we are avoiding that roundtrip by constructing the resulting PSObjects directly + var className = typeAst.Name; + + string cimSuperClassName = null; + if (typeAst.Attributes.Any(a => a.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute))) + { + cimSuperClassName = "OMI_BaseResource"; + } + + var cimClassProperties = ProcessMembers(embeddedInstanceTypes, typeAst, className).ToArray(); + + Queue bases = new Queue(); + foreach (var b in typeAst.BaseTypes) + { + bases.Enqueue(b); + } + + while (bases.Count > 0) + { + var b = bases.Dequeue(); + var tc = b as TypeConstraintAst; + + if (tc is not null) + { + b = tc.TypeName.GetReflectionType(); + if (b is null) + { + var td = tc.TypeName as TypeName; + if (td is not null && td._typeDefinitionAst is not null) + { + ProcessMembers(embeddedInstanceTypes, td._typeDefinitionAst, className); + foreach (var b1 in td._typeDefinitionAst.BaseTypes) + { + bases.Enqueue(b1); + } + } + + continue; + } + } + } + + var result = new PSObject(); + result.Properties.Add(new PSNoteProperty("ClassName", className)); + result.Properties.Add(new PSNoteProperty("FriendlyName", className)); + result.Properties.Add(new PSNoteProperty("SuperClassName", cimSuperClassName)); + result.Properties.Add(new PSNoteProperty("ClassProperties", cimClassProperties)); + + return new[] { result }; + } + + private static List ProcessMembers(List embeddedInstanceTypes, TypeDefinitionAst typeDefinitionAst, string className) + { + List result = new List(); + + foreach (var member in typeDefinitionAst.Members) + { + var property = member as PropertyMemberAst; + + if (property == null || property.IsStatic || + property.Attributes.All(a => a.TypeName.GetReflectionAttributeType() != typeof(DscPropertyAttribute))) + { + continue; + } + + var memberType = property.PropertyType is null + ? typeof(object) + : property.PropertyType.TypeName.GetReflectionType(); + + var attributes = new List(); + for (int i = 0; i < property.Attributes.Count; i++) + { + attributes.Add(property.Attributes[i].GetAttribute()); + } + + string mofType; + bool isArrayType; + string embeddedInstanceType; + string[] enumNames = null; + + if (memberType != null) + { + mofType = MapTypeToMofType(memberType, member.Name, className, out isArrayType, out embeddedInstanceType, embeddedInstanceTypes); + if (memberType.IsEnum) + { + enumNames = Enum.GetNames(memberType); + } + } + else + { + // PropertyType can't be null, we used typeof(object) above in that case so we don't get here. + mofType = MapTypeNameToMofType(property.PropertyType.TypeName, member.Name, className, out isArrayType, out embeddedInstanceType, embeddedInstanceTypes, ref enumNames); + } + + var propertyObject = new PSObject(); + propertyObject.Properties.Add(new PSNoteProperty(@"Name", member.Name)); + propertyObject.Properties.Add(new PSNoteProperty(@"CimType", mofType + (isArrayType ? "Array" : string.Empty))); + if (!string.IsNullOrEmpty(embeddedInstanceType)) + { + propertyObject.Properties.Add(new PSNoteProperty(@"ReferenceClassName", embeddedInstanceType)); + } + + PSObject attributesPSObject = null; + foreach (var attr in attributes) + { + var dscProperty = attr as DscPropertyAttribute; + if (dscProperty is not null) + { + if (attributesPSObject is null) + { + attributesPSObject = new PSObject(); + } + + if (dscProperty.Key) + { + attributesPSObject.Properties.Add(new PSNoteProperty("Key", true)); + } + + if (dscProperty.Mandatory) + { + attributesPSObject.Properties.Add(new PSNoteProperty("Required", true)); + } + + if (dscProperty.NotConfigurable) + { + attributesPSObject.Properties.Add(new PSNoteProperty("Read", true)); + } + + continue; + } + + var validateSet = attr as ValidateSetAttribute; + if (validateSet is not null) + { + if (attributesPSObject is null) + { + attributesPSObject = new PSObject(); + } + + List valueMap = new List(validateSet.ValidValues); + List values = new List(validateSet.ValidValues); + attributesPSObject.Properties.Add(new PSNoteProperty("ValueMap", valueMap)); + attributesPSObject.Properties.Add(new PSNoteProperty("Values", values)); + } + } + + if (attributesPSObject is not null) + { + propertyObject.Properties.Add(new PSNoteProperty(@"Qualifiers", attributesPSObject)); + } + + result.Add(propertyObject); + } + + return result; + } + + private static bool GetResourceDefinitionsFromModule(string fileName, out IEnumerable resourceDefinitions, Collection errorList, IScriptExtent extent) + { + resourceDefinitions = null; + + if (string.IsNullOrEmpty(fileName)) + { + return false; + } + + if (!".psm1".Equals(Path.GetExtension(fileName), StringComparison.OrdinalIgnoreCase) && + !".ps1".Equals(Path.GetExtension(fileName), StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + Token[] tokens; + ParseError[] errors; + var ast = Parser.ParseFile(fileName, out tokens, out errors); + + if (errors is not null && errors.Length > 0) + { + if (errorList is not null && extent is not null) + { + List errorMessages = new List(); + foreach (var error in errors) + { + errorMessages.Add(error.ToString()); + } + + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.FailToParseModuleScriptFile, fileName, string.Join(Environment.NewLine, errorMessages)); + e.SetErrorId("FailToParseModuleScriptFile"); + errorList.Add(e); + } + + return false; + } + + resourceDefinitions = ast.FindAll( + n => + { + var typeAst = n as TypeDefinitionAst; + if (typeAst is not null) + { + for (int i = 0; i < typeAst.Attributes.Count; i++) + { + var a = typeAst.Attributes[i]; + if (a.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute)) + { + return true; + } + } + } + + return false; + }, + false); + + return true; + } + + private static bool LoadPowerShellClassResourcesFromModule(string fileName, PSModuleInfo module, ICollection resourcesToImport, ICollection resourcesFound, Dictionary functionsToDefine, Collection errorList, IScriptExtent extent) + { + IEnumerable resourceDefinitions; + if (!GetResourceDefinitionsFromModule(fileName, out resourceDefinitions, errorList, extent)) + { + return false; + } + + var result = false; + + const WildcardOptions options = WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant; + IEnumerable patternList = SessionStateUtilities.CreateWildcardsFromStrings(module._declaredDscResourceExports, options); + + foreach (var r in resourceDefinitions) + { + result = true; + var resourceDefnAst = (TypeDefinitionAst)r; + + if (!SessionStateUtilities.MatchesAnyWildcardPattern(resourceDefnAst.Name, patternList, true)) + { + continue; + } + + bool skip = true; + foreach (var toImport in resourcesToImport) + { + if (WildcardPattern.Get(toImport, WildcardOptions.IgnoreCase).IsMatch(resourceDefnAst.Name)) + { + skip = false; + break; + } + } + + if (skip) + { + continue; + } + + // Parse the Resource Attribute to see if RunAs behavior is specified for the resource. + DSCResourceRunAsCredential runAsBehavior = DSCResourceRunAsCredential.Default; + foreach (var attr in resourceDefnAst.Attributes) + { + if (attr.TypeName.GetReflectionAttributeType() == typeof(DscResourceAttribute)) + { + foreach (var na in attr.NamedArguments) + { + if (na.ArgumentName.Equals("RunAsCredential", StringComparison.OrdinalIgnoreCase)) + { + var dscResourceAttribute = attr.GetAttribute() as DscResourceAttribute; + if (dscResourceAttribute != null) + { + runAsBehavior = dscResourceAttribute.RunAsCredential; + } + } + } + } + } + + var classes = GenerateJsonClassesForAst(resourceDefnAst, module, runAsBehavior); + + ProcessJsonForDynamicKeywords(module, resourcesFound, functionsToDefine, classes, runAsBehavior, errorList); + } + + return result; + } + + private static readonly Dictionary s_mapPrimitiveDotNetTypeToMof = new Dictionary() + { + { typeof(sbyte), "sint8" }, + { typeof(byte), "uint8" }, + { typeof(short), "sint16" }, + { typeof(ushort), "uint16" }, + { typeof(int), "sint32" }, + { typeof(uint), "uint32" }, + { typeof(long), "sint64" }, + { typeof(ulong), "uint64" }, + { typeof(float), "real32" }, + { typeof(double), "real64" }, + { typeof(bool), "boolean" }, + { typeof(string), "string" }, + { typeof(DateTime), "datetime" }, + { typeof(PSCredential), "string" }, + { typeof(char), "char16" }, + }; + + internal static string MapTypeToMofType(Type type, string memberName, string className, out bool isArrayType, out string embeddedInstanceType, List embeddedInstanceTypes) + { + isArrayType = false; + if (type.IsValueType) + { + type = Nullable.GetUnderlyingType(type) ?? type; + } + + if (type.IsEnum) + { + embeddedInstanceType = null; + return "string"; + } + + if (type == typeof(Hashtable)) + { + // Hashtable is obviously not an array, but in the mof, we represent + // it as string[] (really, embeddedinstance of MSFT_KeyValuePair), but + // we need an array to hold each entry in the hashtable. + isArrayType = true; + embeddedInstanceType = "MSFT_KeyValuePair"; + return "string"; + } + + if (type == typeof(PSCredential)) + { + embeddedInstanceType = "MSFT_Credential"; + return "string"; + } + + if (type.IsArray) + { + isArrayType = true; + bool temp; + var elementType = type.GetElementType(); + if (!elementType.IsArray) + { + return MapTypeToMofType(type.GetElementType(), memberName, className, out temp, out embeddedInstanceType, embeddedInstanceTypes); + } + } + else + { + string cimType; + if (s_mapPrimitiveDotNetTypeToMof.TryGetValue(type, out cimType)) + { + embeddedInstanceType = null; + return cimType; + } + } + + bool supported = false; + bool missingDefaultConstructor = false; + if (type.IsValueType) + { + if (s_mapPrimitiveDotNetTypeToMof.ContainsKey(type)) + { + supported = true; + } + } + else if (!type.IsAbstract) + { + // Must have default constructor, at least 1 public property/field, and no base classes + if (type.GetConstructor(Type.EmptyTypes) is null) + { + missingDefaultConstructor = true; + } + else if (type.BaseType == typeof(object) && + (type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Length > 0 || + type.GetFields(BindingFlags.Instance | BindingFlags.Public).Length > 0)) + { + supported = true; + } + } + + if (supported) + { + if (!embeddedInstanceTypes.Contains(type)) + { + embeddedInstanceTypes.Add(type); + } + + // The type is obviously not a string, but in the mof, we represent + // it as string (really, embeddedinstance of the class type) + embeddedInstanceType = type.FullName.Replace('.', '_'); + return "string"; + } + + if (missingDefaultConstructor) + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + ParserStrings.DscResourceMissingDefaultConstructor, + type.Name)); + } + else + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + ParserStrings.UnsupportedPropertyTypeOfDSCResourceClass, + memberName, + type.Name, + className)); + } + } + + private static void ProcessJsonForDynamicKeywords( + PSModuleInfo module, + ICollection resourcesFound, + Dictionary functionsToDefine, + PSObject[] classes, + DSCResourceRunAsCredential runAsBehavior, + Collection errors) + { + foreach (dynamic c in classes) + { + var className = c.ClassName; + string alias = GetFriendlyName(c); + var friendlyName = string.IsNullOrEmpty(alias) ? className : alias; + if (!CacheResourcesFromMultipleModuleVersions) + { + // Find & remove the previous version of the resource. + List> resourceList = FindResourceInCache(module.Name, className, friendlyName); + + if (resourceList.Count > 0 && !string.IsNullOrEmpty(resourceList[0].Key)) + { + ClassCache.Remove(resourceList[0].Key); + + // keyword is already defined and it is a Inbox resource, remove it + if (DynamicKeyword.ContainsKeyword(friendlyName) && resourceList[0].Value.IsImportedImplicitly) + { + DynamicKeyword.RemoveKeyword(friendlyName); + } + } + } + + var moduleQualifiedResourceName = GetModuleQualifiedResourceName(module.Name, module.Version.ToString(), className, friendlyName); + DscClassCacheEntry existingCacheEntry = null; + if (ClassCache.TryGetValue(moduleQualifiedResourceName, out existingCacheEntry)) + { + if (errors is not null) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateCimClassDefinition, className, module.Path, existingCacheEntry.ModulePath); + e.SetErrorId("DuplicateCimClassDefinition"); + errors.Add(e); + } + } + else + { + var classCacheEntry = new DscClassCacheEntry(runAsBehavior, false, c, module.Path); + ClassCache[moduleQualifiedResourceName] = classCacheEntry; + GuestConfigClassCache[moduleQualifiedResourceName] = classCacheEntry; + ByClassModuleCache[className] = new Tuple(module.Name, module.Version); + resourcesFound.Add(className); + CreateAndRegisterKeywordFromCimClass(module.Name, module.Version, c, functionsToDefine, runAsBehavior); + } + } + } + + /// + /// Returns an error record to use in the case of a malformed resource reference in the DependsOn list. + /// + /// The malformed resource. + /// The referencing resource instance. + /// Generated error record. + public static ErrorRecord GetBadlyFormedRequiredResourceIdErrorRecord(string badDependsOnReference, string definingResource) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetBadlyFormedRequiredResourceId, badDependsOnReference, definingResource); + e.SetErrorId("GetBadlyFormedRequiredResourceId"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of a malformed resource reference in the exclusive resources list. + /// + /// The malformed resource. + /// The referencing resource instance. + /// Generated error record. + public static ErrorRecord GetBadlyFormedExclusiveResourceIdErrorRecord(string badExclusiveResourcereference, string definingResource) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetBadlyFormedExclusiveResourceId, badExclusiveResourcereference, definingResource); + e.SetErrorId("GetBadlyFormedExclusiveResourceId"); + return e.ErrorRecord; + } + + /// + /// If a partial configuration is in 'Pull' Mode, it needs a configuration source. + /// + /// Resource id. + /// Generated error record. + public static ErrorRecord GetPullModeNeedConfigurationSource(string resourceId) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.GetPullModeNeedConfigurationSource, resourceId); + e.SetErrorId("GetPullModeNeedConfigurationSource"); + return e.ErrorRecord; + } + + /// + /// Refresh Mode can not be Disabled for the Partial Configurations. + /// + /// Resource id. + /// Generated error record. + public static ErrorRecord DisabledRefreshModeNotValidForPartialConfig(string resourceId) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DisabledRefreshModeNotValidForPartialConfig, resourceId); + e.SetErrorId("DisabledRefreshModeNotValidForPartialConfig"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of a malformed resource reference in the DependsOn list. + /// + /// The duplicate resource identifier. + /// The node being defined. + /// The error record to use. + public static ErrorRecord DuplicateResourceIdInNodeStatementErrorRecord(string duplicateResourceId, string nodeName) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DuplicateResourceIdInNodeStatement, duplicateResourceId, nodeName); + e.SetErrorId("DuplicateResourceIdInNodeStatement"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of a configuration name is invalid. + /// + /// Configuration name. + /// Generated error record. + public static ErrorRecord InvalidConfigurationNameErrorRecord(string configurationName) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidConfigurationName, configurationName); + e.SetErrorId("InvalidConfigurationName"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of the given value for a property is invalid. + /// + /// Property name. + /// Property value. + /// Keyword name. + /// Valid property values. + /// Generated error record. + public static ErrorRecord InvalidValueForPropertyErrorRecord(string propertyName, string value, string keywordName, string validValues) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidValueForProperty, value, propertyName, keywordName, validValues); + e.SetErrorId("InvalidValueForProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in case the given property is not valid LocalConfigurationManager property. + /// + /// Property name. + /// Valid properties. + /// Generated error record. + public static ErrorRecord InvalidLocalConfigurationManagerPropertyErrorRecord(string propertyName, string validProperties) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.InvalidLocalConfigurationManagerProperty, propertyName, validProperties); + e.SetErrorId("InvalidLocalConfigurationManagerProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of the given value for a property is not supported. + /// + /// Property name. + /// Property value. + /// Keyword name. + /// Valid property values. + /// Generated error record. + public static ErrorRecord UnsupportedValueForPropertyErrorRecord(string propertyName, string value, string keywordName, string validValues) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.UnsupportedValueForProperty, value, propertyName, keywordName, validValues); + e.SetErrorId("UnsupportedValueForProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of no value is provided for a mandatory property. + /// + /// Keyword name. + /// Type name. + /// Property name. + /// Generated error record. + public static ErrorRecord MissingValueForMandatoryPropertyErrorRecord(string keywordName, string typeName, string propertyName) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.MissingValueForMandatoryProperty, keywordName, typeName, propertyName); + e.SetErrorId("MissingValueForMandatoryProperty"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use in the case of more than one values are provided for DebugMode property. + /// + /// Generated error record. + public static ErrorRecord DebugModeShouldHaveOneValue() + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.DebugModeShouldHaveOneValue); + e.SetErrorId("DebugModeShouldHaveOneValue"); + return e.ErrorRecord; + } + + /// + /// Return an error to indicate a value is out of range for a dynamic keyword property. + /// + /// Rroperty name. + /// Resource name. + /// Provided value. + /// Valid range lower bound. + /// Valid range upper bound. + /// Generated error record. + public static ErrorRecord ValueNotInRangeErrorRecord(string property, string name, int providedValue, int lower, int upper) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.ValueNotInRange, property, name, providedValue, lower, upper); + e.SetErrorId("ValueNotInRange"); + return e.ErrorRecord; + } + + /// + /// Returns an error record to use when composite resource and its resource instances both has PsDscRunAsCredentials value. + /// + /// ResourceId of resource. + /// Generated error record. + public static ErrorRecord PsDscRunAsCredentialMergeErrorForCompositeResources(string resourceId) + { + PSInvalidOperationException e = PSTraceSource.NewInvalidOperationException(ParserStrings.PsDscRunAsCredentialMergeErrorForCompositeResources, resourceId); + e.SetErrorId("PsDscRunAsCredentialMergeErrorForCompositeResources"); + return e.ErrorRecord; + } + + /// + /// 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[]] ] + /// } + /// + /// Dynamic keyword. + /// Usage string. + 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(); + } + + /// + /// Format the type name of a CIM property in a presentable way. + /// + /// Dynamic keyword property. + /// If this is optional property or not. + /// CIM property type string. + private static StringBuilder FormatCimPropertyType(DynamicKeywordProperty prop, bool isOptionalProperty) + { + string cimTypeName = prop.TypeConstraint; + StringBuilder formattedTypeString = new StringBuilder(); + + if (string.Equals(cimTypeName, "MSFT_Credential", StringComparison.OrdinalIgnoreCase)) + { + formattedTypeString.Append("[PSCredential]"); + } + else if (string.Equals(cimTypeName, "MSFT_KeyValuePair", StringComparison.OrdinalIgnoreCase) || string.Equals(cimTypeName, "MSFT_KeyValuePair[]", StringComparison.OrdinalIgnoreCase)) + { + formattedTypeString.Append("[Hashtable]"); + } + else + { + string convertedTypeString = System.Management.Automation.LanguagePrimitives.ConvertTypeNameToPSTypeName(cimTypeName); + if (!string.IsNullOrEmpty(convertedTypeString) && !string.Equals(convertedTypeString, "[]", StringComparison.OrdinalIgnoreCase)) + { + formattedTypeString.Append(convertedTypeString); + } + else + { + formattedTypeString.Append("[" + cimTypeName + "]"); + } + } + + // Do the property values map + if (prop.ValueMap is not null && prop.ValueMap.Count > 0) + { + formattedTypeString.Append(" { " + string.Join(" | ", prop.ValueMap.Keys.OrderBy(x => x)) + " }"); + } + + // We prepend optional property with "[" so close out it here. This way it is shown with [ ] to indication optional + if (isOptionalProperty) + { + formattedTypeString.Append(']'); + } + + formattedTypeString.Append('\n'); + + return formattedTypeString; + } + + /// + /// Gets the scriptblock that implements the CIM keyword functionality. + /// + private static ScriptBlock CimKeywordImplementationFunction + { + get + { + // The scriptblock cache will handle mutual exclusion + return s_cimKeywordImplementationFunction ??= ScriptBlock.Create(CimKeywordImplementationFunctionText); + } + } + + private static ScriptBlock s_cimKeywordImplementationFunction; + + private const string CimKeywordImplementationFunctionText = @" + param ( + [Parameter(Mandatory)] + $KeywordData, + [Parameter(Mandatory)] + $Name, + [Parameter(Mandatory)] + [Hashtable] + $Value, + [Parameter(Mandatory)] + $SourceMetadata + ) + +# walk the call stack to get at all of the enclosing configuration resource IDs + $stackedConfigs = @(Get-PSCallStack | + where { ($null -ne $_.InvocationInfo.MyCommand) -and ($_.InvocationInfo.MyCommand.CommandType -eq 'Configuration') }) +# keep all but the top-most + $stackedConfigs = $stackedConfigs[0..(@($stackedConfigs).Length - 2)] +# and build the complex resource ID suffix. + $complexResourceQualifier = ( $stackedConfigs | ForEach-Object { '[' + $_.Command + ']' + $_.InvocationInfo.BoundParameters['InstanceName'] } ) -join '::' + +# +# Utility function used to validate that the DependsOn arguments are well-formed. +# The function also adds them to the define nodes resource collection. +# in the case of resources generated inside a script resource, this routine +# will also fix up the DependsOn references to '[Type]Instance::[OuterType]::OuterInstance +# + function Test-DependsOn + { + +# make sure the references are well-formed + $updatedDependsOn = foreach ($DependsOnVar in $value['DependsOn']) { +# match [ResourceType]ResourceName. ResourceName should starts with [a-z_0-9] followed by [a-z_0-9\p{Zs}\.\\-]* + if ($DependsOnVar -notmatch '^\[[a-z]\w*\][a-z_0-9][a-z_0-9\p{Zs}\.\\-]*$') + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetBadlyFormedRequiredResourceIdErrorRecord($DependsOnVar, $resourceId)) + } + +# Fix up DependsOn for nested names + if ($MyTypeName -and $typeName -ne $MyTypeName -and $InstanceName) + { + ""$DependsOnVar::$complexResourceQualifier"" + } + else + { + $DependsOnVar + } + } + + $value['DependsOn']= $updatedDependsOn + + if($null -ne $DependsOn) + { +# +# Combine DependsOn with dependson from outer composite resource +# which is set as local variable $DependsOn at the composite resource context +# + $value['DependsOn']= @($value['DependsOn']) + $DependsOn + } + +# Save the resource id in a per-node dictionary to do cross validation at the end + Set-NodeResources $resourceId @( $value['DependsOn']) + +# Remove depends on because it need to be fixed up for composite resources +# We do it in ValidateNodeResource and Update-Depends on in configuration/Node function + $value.Remove('DependsOn') + } + +# A copy of the value object with correctly-cased property names + $canonicalizedValue = @{} + + $typeName = $keywordData.ResourceName # CIM type + $keywordName = $keywordData.Keyword # user-friendly alias that is used in scripts + $keyValues = '' + $debugPrefix = "" ${TypeName}:"" # set up a debug prefix string that makes it easier to track what's happening. + + Write-Debug ""${debugPrefix} RESOURCE PROCESSING STARTED [KeywordName='$keywordName'] Function='$($myinvocation.Invocationname)']"" + +# Check whether it's an old style metaconfig + $OldMetaConfig = $false + if ((-not $IsMetaConfig) -and ($keywordName -ieq 'LocalConfigurationManager')) { + $OldMetaConfig = $true + } + +# Check to see if it's a resource keyword. If so add the meta-properties to the canonical property collection. + $resourceId = $null +# todo: need to include configuration managers and partial configuration + if (($keywordData.Properties.Keys -contains 'DependsOn') -or (($KeywordData.ImplementingModule -ieq 'PSDesiredStateConfigurationEngine') -and ($KeywordData.NameMode -eq [System.Management.Automation.Language.DynamicKeywordNameMode]::NameRequired))) + { + + $resourceId = ""[$keywordName]$name"" + if ($MyTypeName -and $keywordName -ne $MyTypeName -and $InstanceName) + { + $resourceId += ""::$complexResourceQualifier"" + } + + Write-Debug ""${debugPrefix} ResourceID = $resourceId"" + +# copy the meta-properties + $canonicalizedValue['ResourceID'] = $resourceId + $canonicalizedValue['SourceInfo'] = $SourceMetadata + if(-not $IsMetaConfig) + { + $canonicalizedValue['ModuleName'] = $keywordData.ImplementingModule + $canonicalizedValue['ModuleVersion'] = $keywordData.ImplementingModuleVersion -as [string] + } + +# see if there is already a resource with this ID. + if (Test-NodeResources $resourceId) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DuplicateResourceIdInNodeStatementErrorRecord($resourceId, (Get-PSCurrentConfigurationNode))) + } + else + { +# If there are prerequisite resources, validate that the references are well-formed strings +# This routine also adds the resource to the global node resources table. + Test-DependsOn + +# Check if PsDscRunCredential is being specified as Arguments to Configuration + if($null -ne $PsDscRunAsCredential) + { +# Check if resource is also trying to set the value for RunAsCred +# In that case we will generate error during compilation, this is merge error + if($null -ne $value['PsDscRunAsCredential']) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::PsDscRunAsCredentialMergeErrorForCompositeResources($resourceId)) + } +# Set the Value of RunAsCred to that of outer configuration + else + { + $value['PsDscRunAsCredential'] = $PsDscRunAsCredential + } + } + +# Save the resource id in a per-node dictionary to do cross validation at the end + if($keywordData.ImplementingModule -ieq ""PSDesiredStateConfigurationEngine"") + { +#$keywordName is PartialConfiguration + if($keywordName -eq 'PartialConfiguration') + { +# RefreshMode is 'Pull' and .ConfigurationSource is empty + if($value['RefreshMode'] -eq 'Pull' -and -not $value['ConfigurationSource']) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetPullModeNeedConfigurationSource($resourceId)) + } + +# Verify that RefreshMode is not Disabled for Partial configuration + if($value['RefreshMode'] -eq 'Disabled') + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DisabledRefreshModeNotValidForPartialConfig($resourceId)) + } + + if($null -ne $value['ConfigurationSource']) + { + Set-NodeManager $resourceId $value['ConfigurationSource'] + } + + if($null -ne $value['ResourceModuleSource']) + { + Set-NodeResourceSource $resourceId $value['ResourceModuleSource'] + } + } + + if($null -ne $value['ExclusiveResources']) + { +# make sure the references are well-formed + foreach ($ExclusiveResource in $value['ExclusiveResources']) { + if (($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*\\[a-z][a-z_0-9]*$') -and ($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*$') -and ($ExclusiveResource -notmatch '^[a-z][a-z_0-9]*\\\*$')) + { + Update-ConfigurationErrorCount + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::GetBadlyFormedExclusiveResourceIdErrorRecord($ExclusiveResource, $resourceId)) + } + } + +# Save the resource id in a per-node dictionary to do cross validation at the end +# Validate resource exist +# Also update the resource reference from module\friendlyname to module\name + $value['ExclusiveResources'] = @(Set-NodeExclusiveResources $resourceId @( $value['ExclusiveResources'] )) + } + } + } + } + else + { + Write-Debug ""${debugPrefix} TYPE IS NOT AS DSC RESOURCE"" + } + +# +# Copy the user-supplied values into a new collection with canonicalized property names +# + foreach ($key in $keywordData.Properties.Keys) + { + Write-Debug ""${debugPrefix} Processing property '$key' ["" + + if ($value.Contains($key)) + { + if ($OldMetaConfig -and (-not ($V1MetaConfigPropertyList -contains $key))) + { + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::InvalidLocalConfigurationManagerPropertyErrorRecord($key, ($V1MetaConfigPropertyList -join ', '))) + Update-ConfigurationErrorCount + } +# see if there is a list of allowed values for this property (similar to an enum) + $allowedValues = $keywordData.Properties[$key].Values +# If there is and user-provided value is not in that list, write an error. + if ($allowedValues) + { + if(($null -eq $value[$key]) -and ($allowedValues -notcontains $value[$key])) + { + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::InvalidValueForPropertyErrorRecord($key, ""$($value[$key])"", $keywordData.Keyword, ($allowedValues -join ', '))) + Update-ConfigurationErrorCount + } + else + { + $notAllowedValue=$null + foreach($v in $value[$key]) + { + if($allowedValues -notcontains $v) + { + $notAllowedValue +=$v.ToString() + ', ' + } + } + + if($notAllowedValue) + { + $notAllowedValue = $notAllowedValue.Substring(0, $notAllowedValue.Length -2) + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::UnsupportedValueForPropertyErrorRecord($key, $notAllowedValue, $keywordData.Keyword, ($allowedValues -join ', '))) + Update-ConfigurationErrorCount + } + } + } + +# see if a value range is defined for this property + $allowedRange = $keywordData.Properties[$key].Range + if($allowedRange) + { + $castedValue = $value[$key] -as [int] + if((($castedValue -is [int]) -and (($castedValue -lt $keywordData.Properties[$key].Range.Item1) -or ($castedValue -gt $keywordData.Properties[$key].Range.Item2))) -or ($null -eq $castedValue)) + { + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::ValueNotInRangeErrorRecord($key, $keywordName, $value[$key], $keywordData.Properties[$key].Range.Item1, $keywordData.Properties[$key].Range.Item2)) + Update-ConfigurationErrorCount + } + } + + Write-Debug ""${debugPrefix} Canonicalized property '$key' = '$($value[$key])'"" + + if ($keywordData.Properties[$key].IsKey) + { + if($null -eq $value[$key]) + { + $keyValues += ""::__NULL__"" + } + else + { + $keyValues += ""::"" + $value[$key] + } + } + +# see if ValueMap is also defined for this property (actual values) + $allowedValueMap = $keywordData.Properties[$key].ValueMap +#if it is and the ValueMap contains the user-provided value as a key, use the actual value + if ($allowedValueMap -and $allowedValueMap.ContainsKey($value[$key])) + { + $canonicalizedValue[$key] = $allowedValueMap[$value[$key]] + } + else + { + $canonicalizedValue[$key] = $value[$key] + } + } + elseif ($keywordData.Properties[$key].Mandatory) + { +# If the property was mandatory but the user didn't provide a value, write and error. + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::MissingValueForMandatoryPropertyErrorRecord($keywordData.Keyword, $keywordData.Properties[$key].TypeConstraint, $Key)) + Update-ConfigurationErrorCount + } + + Write-Debug ""${debugPrefix} Processing completed '$key' ]"" + } + + if($keyValues) + { + $keyValues = $keyValues.Substring(2) # Remove the leading '::' + Add-NodeKeys $keyValues $keywordName + Test-ConflictingResources $keywordName $canonicalizedValue $keywordData + } + +# update OMI_ConfigurationDocument + if($IsMetaConfig) + { + if($keywordData.ResourceName -eq 'OMI_ConfigurationDocument') + { + if($(Get-PSMetaConfigurationProcessed)) + { + $PSMetaConfigDocumentInstVersionInfo = Get-PSMetaConfigDocumentInstVersionInfo + $canonicalizedValue['MinimumCompatibleVersion']=$PSMetaConfigDocumentInstVersionInfo['MinimumCompatibleVersion'] + } + else + { + Set-PSMetaConfigDocInsProcessedBeforeMeta + $canonicalizedValue['MinimumCompatibleVersion']='1.0.0' + } + } + + if(($keywordData.ResourceName -eq 'MSFT_WebDownloadManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_FileDownloadManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_WebResourceManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_FileResourceManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_WebReportManager') ` + -or ($keywordData.ResourceName -eq 'MSFT_SignatureValidation') ` + -or ($keywordData.ResourceName -eq 'MSFT_PartialConfiguration')) + { + Set-PSMetaConfigVersionInfoV2 + } + } + elseif($keywordData.ResourceName -eq 'OMI_ConfigurationDocument') + { + $canonicalizedValue['MinimumCompatibleVersion']='1.0.0' + $canonicalizedValue['CompatibleVersionAdditionalProperties']=@('Omi_BaseResource:ConfigurationName') + } + + if(($keywordData.ResourceName -eq 'MSFT_DSCMetaConfiguration') -or ($keywordData.ResourceName -eq 'MSFT_DSCMetaConfigurationV2')) + { + if($canonicalizedValue['DebugMode'] -and @($canonicalizedValue['DebugMode']).Length -gt 1) + { +# we only allow one value for debug mode now. + Write-Error -ErrorRecord ([Microsoft.PowerShell.DesiredStateConfiguration.Internal.CrossPlatform.DscClassCache]::DebugModeShouldHaveOneValue()) + Update-ConfigurationErrorCount + } + } + +# Generate the MOF text for this resource instance. +# when generate mof text for OMI_ConfigurationDocument we handle below two cases: +# 1. we will add versioning related property based on meta configuration instance already process +# 2. we update the existing OMI_ConfigurationDocument instance if it already exists when process meta configuration instance + $aliasId = ConvertTo-MOFInstance $keywordName $canonicalizedValue + +# If a OMI_ConfigurationDocument is executed outside of a node statement, it becomes the default +# for all nodes that don't have an explicit OMI_ConfigurationDocument declaration + if ($keywordData.ResourceName -eq 'OMI_ConfigurationDocument' -and -not (Get-PSCurrentConfigurationNode)) + { + $data = Get-MoFInstanceText $aliasId + Write-Debug ""${debugPrefix} DEFINING DEFAULT CONFIGURATION DOCUMENT: $data"" + Set-PSDefaultConfigurationDocument $data + } + + Write-Debug ""${debugPrefix} MOF alias for this resource is '$aliasId'"" + +# always return the aliasId so the generated file will be well-formed if not valid + $aliasId + + Write-Debug ""${debugPrefix} RESOURCE PROCESSING COMPLETED. TOTAL ERROR COUNT: $(Get-ConfigurationErrorCount)"" + + "; + } +} diff --git a/src/DscSubsystem/Microsoft.PowerShell.DscSubsystem.csproj b/src/DscSubsystem/Microsoft.PowerShell.DscSubsystem.csproj new file mode 100644 index 0000000..bf5c3d6 --- /dev/null +++ b/src/DscSubsystem/Microsoft.PowerShell.DscSubsystem.csproj @@ -0,0 +1,15 @@ + + + net6.0 + Microsoft.PowerShell.DscSubsystem + + true + visualstudiopublic.snk + true + + + + + + + diff --git a/src/DscSubsystem/visualstudiopublic.snk b/src/DscSubsystem/visualstudiopublic.snk new file mode 100644 index 0000000..695f1b3 Binary files /dev/null and b/src/DscSubsystem/visualstudiopublic.snk differ diff --git a/src/PSDesiredStateConfiguration/PSDesiredStateConfiguration.psd1 b/src/PSDesiredStateConfiguration/PSDesiredStateConfiguration.psd1 index 9925ed5..eb3ae20 100644 --- a/src/PSDesiredStateConfiguration/PSDesiredStateConfiguration.psd1 +++ b/src/PSDesiredStateConfiguration/PSDesiredStateConfiguration.psd1 @@ -48,7 +48,7 @@ PowerShellVersion = '7.1' # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() + #RequiredModules = @() # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() @@ -63,7 +63,7 @@ PowerShellVersion = '7.1' # FormatsToProcess = @() # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + NestedModules = @('Microsoft.PowerShell.DscSubsystem.dll') # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @(