From 7fec0b69a082aaeec72fee0612766aa42f6b1b4d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 18 Apr 2024 07:52:35 +0200 Subject: Merging upstream version 9.4.0+dfsg. Signed-off-by: Daniel Baumann --- .../targets/agent_job_schedule/tasks/main.yml | 6 +- .../tests/integration/targets/login/tasks/main.yml | 13 + .../targets/setup_sqlserver/tasks/main.yml | 3 +- .../targets/setup_sqlserver/vars/main.yml | 9 +- .../connection_plugins/local_pwsh.py | 1 - .../module_utils/Ansible.Basic.cs | 1481 ------------------- .../module_utils/Ansible.ModuleUtils.AddType.psm1 | 397 ------ .../module_utils/_Ansible.Basic.cs | 1489 ++++++++++++++++++++ .../module_utils/_Ansible.ModuleUtils.AddType.psm1 | 397 ++++++ .../shell_plugins/pwsh.py | 44 +- .../targets/setup_win_sqlserver/tasks/main.yml | 3 +- .../targets/win_ag_replica/tasks/main.yml | 5 +- 12 files changed, 1942 insertions(+), 1906 deletions(-) delete mode 100644 ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.Basic.cs delete mode 100644 ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.ModuleUtils.AddType.psm1 create mode 100644 ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.Basic.cs create mode 100644 ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.ModuleUtils.AddType.psm1 (limited to 'ansible_collections/lowlydba/sqlserver/tests') diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/agent_job_schedule/tasks/main.yml b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/agent_job_schedule/tasks/main.yml index 5bd266548..8f0e367ad 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/agent_job_schedule/tasks/main.yml +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/agent_job_schedule/tasks/main.yml @@ -26,7 +26,6 @@ sql_password: "{{ sqlserver_password }}" start_date: "{{ start_date }}" start_time: "{{ start_time }}" - end_date: "{{ end_date }}" end_time: "{{ end_time }}" frequency_type: "{{ frequency_type }}" frequency_interval: "{{ frequency_interval }}" @@ -44,8 +43,9 @@ - assert: that: - result is changed + - result.data.Name == "{{ job_name }}" - - name: Create job schedule with force + - name: Create job schedule with force, no end date lowlydba.sqlserver.agent_job_schedule: schedule: "{{ forced_schedule_name }}" force: true @@ -55,7 +55,7 @@ that: - result.data.ScheduleUid != None - result.data.ActiveStartDate == "3020-05-25T00:00:00.0000000" - - result.data.ActiveEndDate == "3020-05-25T00:00:00.0000000" + - result.data.ActiveEndDate == "9999-12-31T00:00:00.0000000" - result.data.JobCount == 1 - result.data.IsEnabled is true - result.data.ScheduleName == "{{ forced_schedule_name }}" diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/login/tasks/main.yml b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/login/tasks/main.yml index da938fc53..da9b60254 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/login/tasks/main.yml +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/login/tasks/main.yml @@ -6,6 +6,7 @@ password_expiration_enabled: false password_policy_enforced: false password_must_change: false + sid: "0x918315B409D64E4BABB31DF2D9FEA879" enabled: false default_database: "master" language: "us_english" @@ -19,6 +20,7 @@ password: "{{ plain_password }}" password_expiration_enabled: "{{ password_expiration_enabled }}" password_must_change: "{{ password_must_change }}" + sid: "{{ sid }}" enabled: "{{ enabled }}" language: "{{ language }}" state: present @@ -47,6 +49,17 @@ - result.data.Name == "{{ login_name }}" - result.data.DefaultDatabase == "model" + - name: Skip pwd reset + lowlydba.sqlserver.login: + default_database: "model" + password: "ItWasA11ADream!" + skip_password_reset: true + enabled: true + register: result + - assert: + that: + - result is not changed + - name: Drop login lowlydba.sqlserver.login: state: "absent" diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/tasks/main.yml b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/tasks/main.yml index 28c3c1cd7..d01023640 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/tasks/main.yml +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/tasks/main.yml @@ -3,7 +3,6 @@ when: not sqlserver_windows ansible.builtin.command: > pwsh -Command "{{ item }}" - no_log: "{{ ansible_verbosity | int < 3 }}" loop: - - "{{ dbatools_install_cmd }}" - "{{ dbops_install_cmd }}" + - "{{ dbatools_install_cmd }}" diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/vars/main.yml b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/vars/main.yml index 3f8503300..bece0f6fb 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/vars/main.yml +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver/vars/main.yml @@ -1,12 +1,13 @@ --- -dbatools_min_version: 1.1.112 +dbatools_min_version: 2.0.0 dbatools_install_cmd: > if (-not(Get-Module -FullyQualifiedName @{ModuleName='dbatools';ModuleVersion='{{ dbatools_min_version }}'} -ListAvailable)) { - Install-Module dbatools -MinimumVersion {{ dbatools_min_version }} -Force + Install-Module dbatools -MinimumVersion {{ dbatools_min_version }} -Force -SkipPublisherCheck -AllowClobber + Set-DbatoolsInsecureConnection -Scope FileUserLocal } -dbops_min_version: 0.8.0 +dbops_min_version: 0.9.0 dbops_install_cmd: > if (-not(Get-Module -FullyQualifiedName @{ModuleName='dbops';ModuleVersion='{{ dbops_min_version }}'} -ListAvailable)) { - Install-Module dbops -MinimumVersion {{ dbops_min_version }} -Force + Install-Module dbops -MinimumVersion {{ dbops_min_version }} -Force -SkipPublisherCheck -AllowClobber } diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/connection_plugins/local_pwsh.py b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/connection_plugins/local_pwsh.py index 18ec57fd3..138eb1ec6 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/connection_plugins/local_pwsh.py +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/connection_plugins/local_pwsh.py @@ -26,7 +26,6 @@ import subprocess import fcntl import getpass -import ansible.constants as C from ansible.errors import AnsibleError, AnsibleFileNotFound from ansible.module_utils.compat import selectors from ansible.module_utils.six import text_type, binary_type diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.Basic.cs b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.Basic.cs deleted file mode 100644 index 484f75751..000000000 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.Basic.cs +++ /dev/null @@ -1,1481 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Runspaces; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.AccessControl; -using System.Security.Principal; -#if CORECLR -using Newtonsoft.Json; -#else -using System.Web.Script.Serialization; -#endif - -// System.Diagnostics.EventLog.dll reference different versioned dlls that are -// loaded in PSCore, ignore CS1702 so the code will ignore this warning -//NoWarn -Name CS1702 -CLR Core - -//AssemblyReference -Type Newtonsoft.Json.JsonConvert -CLR Core -//AssemblyReference -Type System.Diagnostics.EventLog -CLR Core -//AssemblyReference -Type System.Security.AccessControl.NativeObjectSecurity -CLR Core -//AssemblyReference -Type System.Security.AccessControl.DirectorySecurity -CLR Core -//AssemblyReference -Type System.Security.Principal.IdentityReference -CLR Core - -//AssemblyReference -Name System.Web.Extensions.dll -CLR Framework - -namespace Ansible.Basic -{ - public class AnsibleModule - { - public delegate void ExitHandler(int rc); - public static ExitHandler Exit = new ExitHandler(ExitModule); - - public delegate void WriteLineHandler(string line); - public static WriteLineHandler WriteLine = new WriteLineHandler(WriteLineModule); - - public static bool _DebugArgSpec = false; - - private static List BOOLEANS_TRUE = new List() { "y", "yes", "on", "1", "true", "t", "1.0" }; - private static List BOOLEANS_FALSE = new List() { "n", "no", "off", "0", "false", "f", "0.0" }; - - private string remoteTmp = Path.GetTempPath(); - private string tmpdir = null; - private HashSet noLogValues = new HashSet(); - private List optionsContext = new List(); - private List warnings = new List(); - private List> deprecations = new List>(); - private List cleanupFiles = new List(); - - private Dictionary passVars = new Dictionary() - { - // null values means no mapping, not used in Ansible.Basic.AnsibleModule - { "check_mode", "CheckMode" }, - { "debug", "DebugMode" }, - { "diff", "DiffMode" }, - { "keep_remote_files", "KeepRemoteFiles" }, - { "module_name", "ModuleName" }, - { "no_log", "NoLog" }, - { "remote_tmp", "remoteTmp" }, - { "selinux_special_fs", null }, - { "shell_executable", null }, - { "socket", null }, - { "string_conversion_action", null }, - { "syslog_facility", null }, - { "tmpdir", "tmpdir" }, - { "verbosity", "Verbosity" }, - { "version", "AnsibleVersion" }, - }; - private List passBools = new List() { "check_mode", "debug", "diff", "keep_remote_files", "no_log" }; - private List passInts = new List() { "verbosity" }; - private Dictionary> specDefaults = new Dictionary>() - { - // key - (default, type) - null is freeform - { "apply_defaults", new List() { false, typeof(bool) } }, - { "aliases", new List() { typeof(List), typeof(List) } }, - { "choices", new List() { typeof(List), typeof(List) } }, - { "default", new List() { null, null } }, - { "deprecated_aliases", new List() { typeof(List), typeof(List) } }, - { "elements", new List() { null, null } }, - { "mutually_exclusive", new List() { typeof(List>), typeof(List) } }, - { "no_log", new List() { false, typeof(bool) } }, - { "options", new List() { typeof(Hashtable), typeof(Hashtable) } }, - { "removed_in_version", new List() { null, typeof(string) } }, - { "removed_at_date", new List() { null, typeof(DateTime) } }, - { "removed_from_collection", new List() { null, typeof(string) } }, - { "required", new List() { false, typeof(bool) } }, - { "required_by", new List() { typeof(Hashtable), typeof(Hashtable) } }, - { "required_if", new List() { typeof(List>), typeof(List) } }, - { "required_one_of", new List() { typeof(List>), typeof(List) } }, - { "required_together", new List() { typeof(List>), typeof(List) } }, - { "supports_check_mode", new List() { false, typeof(bool) } }, - { "type", new List() { "str", null } }, - }; - private Dictionary optionTypes = new Dictionary() - { - { "bool", new Func(ParseBool) }, - { "dict", new Func>(ParseDict) }, - { "float", new Func(ParseFloat) }, - { "int", new Func(ParseInt) }, - { "json", new Func(ParseJson) }, - { "list", new Func>(ParseList) }, - { "path", new Func(ParsePath) }, - { "raw", new Func(ParseRaw) }, - { "sid", new Func(ParseSid) }, - { "str", new Func(ParseStr) }, - }; - - public Dictionary Diff = new Dictionary(); - public IDictionary Params = null; - public Dictionary Result = new Dictionary() { { "changed", false } }; - - public bool CheckMode { get; private set; } - public bool DebugMode { get; private set; } - public bool DiffMode { get; private set; } - public bool KeepRemoteFiles { get; private set; } - public string ModuleName { get; private set; } - public bool NoLog { get; private set; } - public int Verbosity { get; private set; } - public string AnsibleVersion { get; private set; } - - public string Tmpdir - { - get - { - if (tmpdir == null) - { - SecurityIdentifier user = WindowsIdentity.GetCurrent().User; - DirectorySecurity dirSecurity = new DirectorySecurity(); - dirSecurity.SetOwner(user); - dirSecurity.SetAccessRuleProtection(true, false); // disable inheritance rules - FileSystemAccessRule ace = new FileSystemAccessRule(user, FileSystemRights.FullControl, - InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, - PropagationFlags.None, AccessControlType.Allow); - dirSecurity.AddAccessRule(ace); - - string baseDir = Path.GetFullPath(Environment.ExpandEnvironmentVariables(remoteTmp)); - if (!Directory.Exists(baseDir)) - { - string failedMsg = null; - try - { -#if CORECLR - DirectoryInfo createdDir = Directory.CreateDirectory(baseDir); - FileSystemAclExtensions.SetAccessControl(createdDir, dirSecurity); -#else - Directory.CreateDirectory(baseDir, dirSecurity); -#endif - } - catch (Exception e) - { - failedMsg = String.Format("Failed to create base tmpdir '{0}': {1}", baseDir, e.Message); - } - - if (failedMsg != null) - { - string envTmp = Path.GetTempPath(); - Warn(String.Format("Unable to use '{0}' as temporary directory, falling back to system tmp '{1}': {2}", baseDir, envTmp, failedMsg)); - baseDir = envTmp; - } - else - { - NTAccount currentUser = (NTAccount)user.Translate(typeof(NTAccount)); - string warnMsg = String.Format("Module remote_tmp {0} did not exist and was created with FullControl to {1}, ", baseDir, currentUser.ToString()); - warnMsg += "this may cause issues when running as another user. To avoid this, create the remote_tmp dir with the correct permissions manually"; - Warn(warnMsg); - } - } - - string dateTime = DateTime.Now.ToFileTime().ToString(); - string dirName = String.Format("ansible-moduletmp-{0}-{1}", dateTime, new Random().Next(0, int.MaxValue)); - string newTmpdir = Path.Combine(baseDir, dirName); -#if CORECLR - DirectoryInfo tmpdirInfo = Directory.CreateDirectory(newTmpdir); - FileSystemAclExtensions.SetAccessControl(tmpdirInfo, dirSecurity); -#else - Directory.CreateDirectory(newTmpdir, dirSecurity); -#endif - tmpdir = newTmpdir; - - if (!KeepRemoteFiles) - cleanupFiles.Add(tmpdir); - } - return tmpdir; - } - } - - public AnsibleModule(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null) - { - // NoLog is not set yet, we cannot rely on FailJson to sanitize the output - // Do the minimum amount to get this running before we actually parse the params - Dictionary aliases = new Dictionary(); - try - { - ValidateArgumentSpec(argumentSpec); - - // Merge the fragments if present into the main arg spec. - if (fragments != null) - { - foreach (IDictionary fragment in fragments) - { - ValidateArgumentSpec(fragment); - MergeFragmentSpec(argumentSpec, fragment); - } - } - - // Used by ansible-test to retrieve the module argument spec, not designed for public use. - if (_DebugArgSpec) - { - // Cannot call exit here because it will be caught with the catch (Exception e) below. Instead - // just throw a new exception with a specific message and the exception block will handle it. - ScriptBlock.Create("Set-Variable -Name ansibleTestArgSpec -Value $args[0] -Scope Global" - ).Invoke(argumentSpec); - throw new Exception("ansible-test validate-modules check"); - } - - // Now make sure all the metadata keys are set to their defaults, this must be done after we've - // potentially output the arg spec for ansible-test. - SetArgumentSpecDefaults(argumentSpec); - - Params = GetParams(args); - aliases = GetAliases(argumentSpec, Params); - SetNoLogValues(argumentSpec, Params); - } - catch (Exception e) - { - if (e.Message == "ansible-test validate-modules check") - Exit(0); - - Dictionary result = new Dictionary - { - { "failed", true }, - { "msg", String.Format("internal error: {0}", e.Message) }, - { "exception", e.ToString() } - }; - WriteLine(ToJson(result)); - Exit(1); - } - - // Initialise public properties to the defaults before we parse the actual inputs - CheckMode = false; - DebugMode = false; - DiffMode = false; - KeepRemoteFiles = false; - ModuleName = "undefined win module"; - NoLog = (bool)argumentSpec["no_log"]; - Verbosity = 0; - AppDomain.CurrentDomain.ProcessExit += CleanupFiles; - - List legalInputs = passVars.Keys.Select(v => "_ansible_" + v).ToList(); - legalInputs.AddRange(((IDictionary)argumentSpec["options"]).Keys.Cast().ToList()); - legalInputs.AddRange(aliases.Keys.Cast().ToList()); - CheckArguments(argumentSpec, Params, legalInputs); - - // Set a Ansible friendly invocation value in the result object - Dictionary invocation = new Dictionary() { { "module_args", Params } }; - Result["invocation"] = RemoveNoLogValues(invocation, noLogValues); - - if (!NoLog) - LogEvent(String.Format("Invoked with:\r\n {0}", FormatLogData(Params, 2)), sanitise: false); - } - - public static AnsibleModule Create(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null) - { - return new AnsibleModule(args, argumentSpec, fragments); - } - - public void Debug(string message) - { - if (DebugMode) - LogEvent(String.Format("[DEBUG] {0}", message)); - } - - public void Deprecate(string message, string version) - { - Deprecate(message, version, null); - } - - public void Deprecate(string message, string version, string collectionName) - { - deprecations.Add(new Dictionary() { - { "msg", message }, { "version", version }, { "collection_name", collectionName } }); - LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, version)); - } - - public void Deprecate(string message, DateTime date) - { - Deprecate(message, date, null); - } - - public void Deprecate(string message, DateTime date, string collectionName) - { - string isoDate = date.ToString("yyyy-MM-dd"); - deprecations.Add(new Dictionary() { - { "msg", message }, { "date", isoDate }, { "collection_name", collectionName } }); - LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, isoDate)); - } - - public void ExitJson() - { - WriteLine(GetFormattedResults(Result)); - CleanupFiles(null, null); - Exit(0); - } - - public void FailJson(string message) { FailJson(message, null, null); } - public void FailJson(string message, ErrorRecord psErrorRecord) { FailJson(message, psErrorRecord, null); } - public void FailJson(string message, Exception exception) { FailJson(message, null, exception); } - private void FailJson(string message, ErrorRecord psErrorRecord, Exception exception) - { - Result["failed"] = true; - Result["msg"] = RemoveNoLogValues(message, noLogValues); - - - if (!Result.ContainsKey("exception") && (Verbosity > 2 || DebugMode)) - { - if (psErrorRecord != null) - { - string traceback = String.Format("{0}\r\n{1}", psErrorRecord.ToString(), psErrorRecord.InvocationInfo.PositionMessage); - traceback += String.Format("\r\n + CategoryInfo : {0}", psErrorRecord.CategoryInfo.ToString()); - traceback += String.Format("\r\n + FullyQualifiedErrorId : {0}", psErrorRecord.FullyQualifiedErrorId.ToString()); - traceback += String.Format("\r\n\r\nScriptStackTrace:\r\n{0}", psErrorRecord.ScriptStackTrace); - Result["exception"] = traceback; - } - else if (exception != null) - Result["exception"] = exception.ToString(); - } - - WriteLine(GetFormattedResults(Result)); - CleanupFiles(null, null); - Exit(1); - } - - public void LogEvent(string message, EventLogEntryType logEntryType = EventLogEntryType.Information, bool sanitise = true) - { - // non-Windows hack; event log is not supported, not implementing a x-plat compat logger at this time - // original content left as comment, because it may make it easier to update this - return; - /* - if (NoLog) - return; - - string logSource = "Ansible"; - bool logSourceExists = false; - try - { - logSourceExists = EventLog.SourceExists(logSource); - } - catch (System.Security.SecurityException) { } // non admin users may not have permission - - if (!logSourceExists) - { - try - { - EventLog.CreateEventSource(logSource, "Application"); - } - catch (System.Security.SecurityException) - { - // Cannot call Warn as that calls LogEvent and we get stuck in a loop - warnings.Add(String.Format("Access error when creating EventLog source {0}, logging to the Application source instead", logSource)); - logSource = "Application"; - } - } - if (sanitise) - message = (string)RemoveNoLogValues(message, noLogValues); - message = String.Format("{0} - {1}", ModuleName, message); - - using (EventLog eventLog = new EventLog("Application")) - { - eventLog.Source = logSource; - try - { - eventLog.WriteEntry(message, logEntryType, 0); - } - catch (System.InvalidOperationException) { } // Ignore permission errors on the Application event log - catch (System.Exception e) - { - // Cannot call Warn as that calls LogEvent and we get stuck in a loop - warnings.Add(String.Format("Unknown error when creating event log entry: {0}", e.Message)); - } - } - */ - } - - public void Warn(string message) - { - warnings.Add(message); - LogEvent(String.Format("[WARNING] {0}", message), EventLogEntryType.Warning); - } - - public static object FromJson(string json) { return FromJson(json); } - public static T FromJson(string json) - { -#if CORECLR - return JsonConvert.DeserializeObject(json); -#else - JavaScriptSerializer jss = new JavaScriptSerializer(); - jss.MaxJsonLength = int.MaxValue; - jss.RecursionLimit = int.MaxValue; - return jss.Deserialize(json); -#endif - } - - public static string ToJson(object obj) - { - // Using PowerShell to serialize the JSON is preferable over the native .NET libraries as it handles - // PS Objects a lot better than the alternatives. In case we are debugging in Visual Studio we have a - // fallback to the other libraries as we won't be dealing with PowerShell objects there. - if (Runspace.DefaultRunspace != null) - { - PSObject rawOut = ScriptBlock.Create("ConvertTo-Json -InputObject $args[0] -Depth 99 -Compress").Invoke(obj)[0]; - return rawOut.BaseObject as string; - } - else - { -#if CORECLR - return JsonConvert.SerializeObject(obj); -#else - JavaScriptSerializer jss = new JavaScriptSerializer(); - jss.MaxJsonLength = int.MaxValue; - jss.RecursionLimit = int.MaxValue; - return jss.Serialize(obj); -#endif - } - } - - public static IDictionary GetParams(string[] args) - { - if (args.Length > 0) - { - string inputJson = File.ReadAllText(args[0]); - Dictionary rawParams = FromJson>(inputJson); - if (!rawParams.ContainsKey("ANSIBLE_MODULE_ARGS")) - throw new ArgumentException("Module was unable to get ANSIBLE_MODULE_ARGS value from the argument path json"); - return (IDictionary)rawParams["ANSIBLE_MODULE_ARGS"]; - } - else - { - // $complex_args is already a Hashtable, no need to waste time converting to a dictionary - PSObject rawArgs = ScriptBlock.Create("$complex_args").Invoke()[0]; - return rawArgs.BaseObject as Hashtable; - } - } - - public static bool ParseBool(object value) - { - if (value.GetType() == typeof(bool)) - return (bool)value; - - List booleans = new List(); - booleans.AddRange(BOOLEANS_TRUE); - booleans.AddRange(BOOLEANS_FALSE); - - string stringValue = ParseStr(value).ToLowerInvariant().Trim(); - if (BOOLEANS_TRUE.Contains(stringValue)) - return true; - else if (BOOLEANS_FALSE.Contains(stringValue)) - return false; - - string msg = String.Format("The value '{0}' is not a valid boolean. Valid booleans include: {1}", - stringValue, String.Join(", ", booleans)); - throw new ArgumentException(msg); - } - - public static Dictionary ParseDict(object value) - { - Type valueType = value.GetType(); - if (valueType == typeof(Dictionary)) - return (Dictionary)value; - else if (value is IDictionary) - return ((IDictionary)value).Cast().ToDictionary(kvp => (string)kvp.Key, kvp => kvp.Value); - else if (valueType == typeof(string)) - { - string stringValue = (string)value; - if (stringValue.StartsWith("{") && stringValue.EndsWith("}")) - return FromJson>((string)value); - else if (stringValue.IndexOfAny(new char[1] { '=' }) != -1) - { - List fields = new List(); - List fieldBuffer = new List(); - char? inQuote = null; - bool inEscape = false; - string field; - - foreach (char c in stringValue.ToCharArray()) - { - if (inEscape) - { - fieldBuffer.Add(c); - inEscape = false; - } - else if (c == '\\') - inEscape = true; - else if (inQuote == null && (c == '\'' || c == '"')) - inQuote = c; - else if (inQuote != null && c == inQuote) - inQuote = null; - else if (inQuote == null && (c == ',' || c == ' ')) - { - field = String.Join("", fieldBuffer); - if (field != "") - fields.Add(field); - fieldBuffer = new List(); - } - else - fieldBuffer.Add(c); - } - - field = String.Join("", fieldBuffer); - if (field != "") - fields.Add(field); - - return fields.Distinct().Select(i => i.Split(new[] { '=' }, 2)).ToDictionary(i => i[0], i => i.Length > 1 ? (object)i[1] : null); - } - else - throw new ArgumentException("string cannot be converted to a dict, must either be a JSON string or in the key=value form"); - } - - throw new ArgumentException(String.Format("{0} cannot be converted to a dict", valueType.FullName)); - } - - public static float ParseFloat(object value) - { - if (value.GetType() == typeof(float)) - return (float)value; - - string valueStr = ParseStr(value); - return float.Parse(valueStr); - } - - public static int ParseInt(object value) - { - Type valueType = value.GetType(); - if (valueType == typeof(int)) - return (int)value; - else - return Int32.Parse(ParseStr(value)); - } - - public static string ParseJson(object value) - { - // mostly used to ensure a dict is a json string as it may - // have been converted on the controller side - Type valueType = value.GetType(); - if (value is IDictionary) - return ToJson(value); - else if (valueType == typeof(string)) - return (string)value; - else - throw new ArgumentException(String.Format("{0} cannot be converted to json", valueType.FullName)); - } - - public static List ParseList(object value) - { - if (value == null) - return null; - - Type valueType = value.GetType(); - if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(List<>)) - return (List)value; - else if (valueType == typeof(ArrayList)) - return ((ArrayList)value).Cast().ToList(); - else if (valueType.IsArray) - return ((object[])value).ToList(); - else if (valueType == typeof(string)) - return ((string)value).Split(',').Select(s => s.Trim()).ToList(); - else if (valueType == typeof(int)) - return new List() { value }; - else - throw new ArgumentException(String.Format("{0} cannot be converted to a list", valueType.FullName)); - } - - public static string ParsePath(object value) - { - string stringValue = ParseStr(value); - - // do not validate, expand the env vars if it starts with \\?\ as - // it is a special path designed for the NT kernel to interpret - if (stringValue.StartsWith(@"\\?\")) - return stringValue; - - stringValue = Environment.ExpandEnvironmentVariables(stringValue); - if (stringValue.IndexOfAny(Path.GetInvalidPathChars()) != -1) - throw new ArgumentException("string value contains invalid path characters, cannot convert to path"); - - // will fire an exception if it contains any invalid chars - Path.GetFullPath(stringValue); - return stringValue; - } - - public static object ParseRaw(object value) { return value; } - - public static SecurityIdentifier ParseSid(object value) - { - string stringValue = ParseStr(value); - - try - { - return new SecurityIdentifier(stringValue); - } - catch (ArgumentException) { } // ignore failures string may not have been a SID - - NTAccount account = new NTAccount(stringValue); - return (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); - } - - public static string ParseStr(object value) { return value.ToString(); } - - private void ValidateArgumentSpec(IDictionary argumentSpec) - { - Dictionary changedValues = new Dictionary(); - foreach (DictionaryEntry entry in argumentSpec) - { - string key = (string)entry.Key; - - // validate the key is a valid argument spec key - if (!specDefaults.ContainsKey(key)) - { - string msg = String.Format("argument spec entry contains an invalid key '{0}', valid keys: {1}", - key, String.Join(", ", specDefaults.Keys)); - throw new ArgumentException(FormatOptionsContext(msg, " - ")); - } - - // ensure the value is casted to the type we expect - Type optionType = null; - if (entry.Value != null) - optionType = (Type)specDefaults[key][1]; - if (optionType != null) - { - Type actualType = entry.Value.GetType(); - bool invalid = false; - if (optionType.IsGenericType && optionType.GetGenericTypeDefinition() == typeof(List<>)) - { - // verify the actual type is not just a single value of the list type - Type entryType = optionType.GetGenericArguments()[0]; - object[] arrayElementTypes = new object[] - { - null, // ArrayList does not have an ElementType - entryType, - typeof(object), // Hope the object is actually entryType or it can at least be casted. - }; - - bool isArray = entry.Value is IList && arrayElementTypes.Contains(actualType.GetElementType()); - if (actualType == entryType || isArray) - { - object rawArray; - if (isArray) - rawArray = entry.Value; - else - rawArray = new object[1] { entry.Value }; - - MethodInfo castMethod = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(entryType); - MethodInfo toListMethod = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(entryType); - - var enumerable = castMethod.Invoke(null, new object[1] { rawArray }); - var newList = toListMethod.Invoke(null, new object[1] { enumerable }); - changedValues.Add(key, newList); - } - else if (actualType != optionType && !(actualType == typeof(List))) - invalid = true; - } - else - invalid = actualType != optionType; - - if (invalid) - { - string msg = String.Format("argument spec for '{0}' did not match expected type {1}: actual type {2}", - key, optionType.FullName, actualType.FullName); - throw new ArgumentException(FormatOptionsContext(msg, " - ")); - } - } - - // recursively validate the spec - if (key == "options" && entry.Value != null) - { - IDictionary optionsSpec = (IDictionary)entry.Value; - foreach (DictionaryEntry optionEntry in optionsSpec) - { - optionsContext.Add((string)optionEntry.Key); - IDictionary optionMeta = (IDictionary)optionEntry.Value; - ValidateArgumentSpec(optionMeta); - optionsContext.RemoveAt(optionsContext.Count - 1); - } - } - - // validate the type and elements key type values are known types - if (key == "type" || key == "elements" && entry.Value != null) - { - Type valueType = entry.Value.GetType(); - if (valueType == typeof(string)) - { - string typeValue = (string)entry.Value; - if (!optionTypes.ContainsKey(typeValue)) - { - string msg = String.Format("{0} '{1}' is unsupported", key, typeValue); - msg = String.Format("{0}. Valid types are: {1}", FormatOptionsContext(msg, " - "), String.Join(", ", optionTypes.Keys)); - throw new ArgumentException(msg); - } - } - else if (!(entry.Value is Delegate)) - { - string msg = String.Format("{0} must either be a string or delegate, was: {1}", key, valueType.FullName); - throw new ArgumentException(FormatOptionsContext(msg, " - ")); - } - } - } - - // Outside of the spec iterator, change the values that were casted above - foreach (KeyValuePair changedValue in changedValues) - argumentSpec[changedValue.Key] = changedValue.Value; - } - - private void MergeFragmentSpec(IDictionary argumentSpec, IDictionary fragment) - { - foreach (DictionaryEntry fragmentEntry in fragment) - { - string fragmentKey = fragmentEntry.Key.ToString(); - - if (argumentSpec.Contains(fragmentKey)) - { - // We only want to add new list entries and merge dictionary new keys and values. Leave the other - // values as is in the argument spec as that takes priority over the fragment. - if (fragmentEntry.Value is IDictionary) - { - MergeFragmentSpec((IDictionary)argumentSpec[fragmentKey], (IDictionary)fragmentEntry.Value); - } - else if (fragmentEntry.Value is IList) - { - IList specValue = (IList)argumentSpec[fragmentKey]; - foreach (object fragmentValue in (IList)fragmentEntry.Value) - specValue.Add(fragmentValue); - } - } - else - argumentSpec[fragmentKey] = fragmentEntry.Value; - } - } - - private void SetArgumentSpecDefaults(IDictionary argumentSpec) - { - foreach (KeyValuePair> metadataEntry in specDefaults) - { - List defaults = metadataEntry.Value; - object defaultValue = defaults[0]; - if (defaultValue != null && defaultValue.GetType() == typeof(Type).GetType()) - defaultValue = Activator.CreateInstance((Type)defaultValue); - - if (!argumentSpec.Contains(metadataEntry.Key)) - argumentSpec[metadataEntry.Key] = defaultValue; - } - - // Recursively set the defaults for any inner options. - foreach (DictionaryEntry entry in argumentSpec) - { - if (entry.Value == null || entry.Key.ToString() != "options") - continue; - - IDictionary optionsSpec = (IDictionary)entry.Value; - foreach (DictionaryEntry optionEntry in optionsSpec) - { - optionsContext.Add((string)optionEntry.Key); - IDictionary optionMeta = (IDictionary)optionEntry.Value; - SetArgumentSpecDefaults(optionMeta); - optionsContext.RemoveAt(optionsContext.Count - 1); - } - } - } - - private Dictionary GetAliases(IDictionary argumentSpec, IDictionary parameters) - { - Dictionary aliasResults = new Dictionary(); - - foreach (DictionaryEntry entry in (IDictionary)argumentSpec["options"]) - { - string k = (string)entry.Key; - Hashtable v = (Hashtable)entry.Value; - - List aliases = (List)v["aliases"]; - object defaultValue = v["default"]; - bool required = (bool)v["required"]; - - if (defaultValue != null && required) - throw new ArgumentException(String.Format("required and default are mutually exclusive for {0}", k)); - - foreach (string alias in aliases) - { - aliasResults.Add(alias, k); - if (parameters.Contains(alias)) - parameters[k] = parameters[alias]; - } - - List deprecatedAliases = (List)v["deprecated_aliases"]; - foreach (Hashtable depInfo in deprecatedAliases) - { - foreach (string keyName in new List { "name" }) - { - if (!depInfo.ContainsKey(keyName)) - { - string msg = String.Format("{0} is required in a deprecated_aliases entry", keyName); - throw new ArgumentException(FormatOptionsContext(msg, " - ")); - } - } - if (!depInfo.ContainsKey("version") && !depInfo.ContainsKey("date")) - { - string msg = "One of version or date is required in a deprecated_aliases entry"; - throw new ArgumentException(FormatOptionsContext(msg, " - ")); - } - if (depInfo.ContainsKey("version") && depInfo.ContainsKey("date")) - { - string msg = "Only one of version or date is allowed in a deprecated_aliases entry"; - throw new ArgumentException(FormatOptionsContext(msg, " - ")); - } - if (depInfo.ContainsKey("date") && depInfo["date"].GetType() != typeof(DateTime)) - { - string msg = "A deprecated_aliases date must be a DateTime object"; - throw new ArgumentException(FormatOptionsContext(msg, " - ")); - } - string collectionName = null; - if (depInfo.ContainsKey("collection_name")) - { - collectionName = (string)depInfo["collection_name"]; - } - string aliasName = (string)depInfo["name"]; - - if (parameters.Contains(aliasName)) - { - string msg = String.Format("Alias '{0}' is deprecated. See the module docs for more information", aliasName); - if (depInfo.ContainsKey("version")) - { - string depVersion = (string)depInfo["version"]; - Deprecate(FormatOptionsContext(msg, " - "), depVersion, collectionName); - } - if (depInfo.ContainsKey("date")) - { - DateTime depDate = (DateTime)depInfo["date"]; - Deprecate(FormatOptionsContext(msg, " - "), depDate, collectionName); - } - } - } - } - - return aliasResults; - } - - private void SetNoLogValues(IDictionary argumentSpec, IDictionary parameters) - { - foreach (DictionaryEntry entry in (IDictionary)argumentSpec["options"]) - { - string k = (string)entry.Key; - Hashtable v = (Hashtable)entry.Value; - - if ((bool)v["no_log"]) - { - object noLogObject = parameters.Contains(k) ? parameters[k] : null; - string noLogString = noLogObject == null ? "" : noLogObject.ToString(); - if (!String.IsNullOrEmpty(noLogString)) - noLogValues.Add(noLogString); - } - string collectionName = null; - if (v.ContainsKey("removed_from_collection")) - { - collectionName = (string)v["removed_from_collection"]; - } - - object removedInVersion = v["removed_in_version"]; - if (removedInVersion != null && parameters.Contains(k)) - Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k), - removedInVersion.ToString(), collectionName); - - object removedAtDate = v["removed_at_date"]; - if (removedAtDate != null && parameters.Contains(k)) - Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k), - (DateTime)removedAtDate, collectionName); - } - } - - private void CheckArguments(IDictionary spec, IDictionary param, List legalInputs) - { - // initially parse the params and check for unsupported ones and set internal vars - CheckUnsupportedArguments(param, legalInputs); - - // Only run this check if we are at the root argument (optionsContext.Count == 0) - if (CheckMode && !(bool)spec["supports_check_mode"] && optionsContext.Count == 0) - { - Result["skipped"] = true; - Result["msg"] = String.Format("remote module ({0}) does not support check mode", ModuleName); - ExitJson(); - } - IDictionary optionSpec = (IDictionary)spec["options"]; - - CheckMutuallyExclusive(param, (IList)spec["mutually_exclusive"]); - CheckRequiredArguments(optionSpec, param); - - // set the parameter types based on the type spec value - foreach (DictionaryEntry entry in optionSpec) - { - string k = (string)entry.Key; - Hashtable v = (Hashtable)entry.Value; - - object value = param.Contains(k) ? param[k] : null; - if (value != null) - { - // convert the current value to the wanted type - Delegate typeConverter; - string type; - if (v["type"].GetType() == typeof(string)) - { - type = (string)v["type"]; - typeConverter = optionTypes[type]; - } - else - { - type = "delegate"; - typeConverter = (Delegate)v["type"]; - } - - try - { - value = typeConverter.DynamicInvoke(value); - param[k] = value; - } - catch (Exception e) - { - string msg = String.Format("argument for {0} is of type {1} and we were unable to convert to {2}: {3}", - k, value.GetType(), type, e.InnerException.Message); - FailJson(FormatOptionsContext(msg)); - } - - // ensure it matches the choices if there are choices set - List choices = ((List)v["choices"]).Select(x => x.ToString()).Cast().ToList(); - if (choices.Count > 0) - { - List values; - string choiceMsg; - if (type == "list") - { - values = ((List)value).Select(x => x.ToString()).Cast().ToList(); - choiceMsg = "one or more of"; - } - else - { - values = new List() { value.ToString() }; - choiceMsg = "one of"; - } - - List diffList = values.Except(choices, StringComparer.OrdinalIgnoreCase).ToList(); - List caseDiffList = values.Except(choices).ToList(); - if (diffList.Count > 0) - { - string msg = String.Format("value of {0} must be {1}: {2}. Got no match for: {3}", - k, choiceMsg, String.Join(", ", choices), String.Join(", ", diffList)); - FailJson(FormatOptionsContext(msg)); - } - /* - For now we will just silently accept case insensitive choices, uncomment this if we want to add it back in - else if (caseDiffList.Count > 0) - { - // For backwards compatibility with Legacy.psm1 we need to be matching choices that are not case sensitive. - // We will warn the user it was case insensitive and tell them this will become case sensitive in the future. - string msg = String.Format( - "value of {0} was a case insensitive match of {1}: {2}. Checking of choices will be case sensitive in a future Ansible release. Case insensitive matches were: {3}", - k, choiceMsg, String.Join(", ", choices), String.Join(", ", caseDiffList.Select(x => RemoveNoLogValues(x, noLogValues))) - ); - Warn(FormatOptionsContext(msg)); - }*/ - } - } - } - - CheckRequiredTogether(param, (IList)spec["required_together"]); - CheckRequiredOneOf(param, (IList)spec["required_one_of"]); - CheckRequiredIf(param, (IList)spec["required_if"]); - CheckRequiredBy(param, (IDictionary)spec["required_by"]); - - // finally ensure all missing parameters are set to null and handle sub options - foreach (DictionaryEntry entry in optionSpec) - { - string k = (string)entry.Key; - IDictionary v = (IDictionary)entry.Value; - - if (!param.Contains(k)) - param[k] = null; - - CheckSubOption(param, k, v); - } - } - - private void CheckUnsupportedArguments(IDictionary param, List legalInputs) - { - HashSet unsupportedParameters = new HashSet(); - HashSet caseUnsupportedParameters = new HashSet(); - List removedParameters = new List(); - - foreach (DictionaryEntry entry in param) - { - string paramKey = (string)entry.Key; - if (!legalInputs.Contains(paramKey, StringComparer.OrdinalIgnoreCase)) - unsupportedParameters.Add(paramKey); - else if (!legalInputs.Contains(paramKey)) - // For backwards compatibility we do not care about the case but we need to warn the users as this will - // change in a future Ansible release. - caseUnsupportedParameters.Add(paramKey); - else if (paramKey.StartsWith("_ansible_")) - { - removedParameters.Add(paramKey); - string key = paramKey.Replace("_ansible_", ""); - // skip setting NoLog if NoLog is already set to true (set by the module) - // or there's no mapping for this key - if ((key == "no_log" && NoLog == true) || (passVars[key] == null)) - continue; - - object value = entry.Value; - if (passBools.Contains(key)) - value = ParseBool(value); - else if (passInts.Contains(key)) - value = ParseInt(value); - - string propertyName = passVars[key]; - PropertyInfo property = typeof(AnsibleModule).GetProperty(propertyName); - FieldInfo field = typeof(AnsibleModule).GetField(propertyName, BindingFlags.NonPublic | BindingFlags.Instance); - if (property != null) - property.SetValue(this, value, null); - else if (field != null) - field.SetValue(this, value); - else - FailJson(String.Format("implementation error: unknown AnsibleModule property {0}", propertyName)); - } - } - foreach (string parameter in removedParameters) - param.Remove(parameter); - - if (unsupportedParameters.Count > 0) - { - legalInputs.RemoveAll(x => passVars.Keys.Contains(x.Replace("_ansible_", ""))); - string msg = String.Format("Unsupported parameters for ({0}) module: {1}", ModuleName, String.Join(", ", unsupportedParameters)); - msg = String.Format("{0}. Supported parameters include: {1}", FormatOptionsContext(msg), String.Join(", ", legalInputs)); - FailJson(msg); - } - - /* - // Uncomment when we want to start warning users around options that are not a case sensitive match to the spec - if (caseUnsupportedParameters.Count > 0) - { - legalInputs.RemoveAll(x => passVars.Keys.Contains(x.Replace("_ansible_", ""))); - string msg = String.Format("Parameters for ({0}) was a case insensitive match: {1}", ModuleName, String.Join(", ", caseUnsupportedParameters)); - msg = String.Format("{0}. Module options will become case sensitive in a future Ansible release. Supported parameters include: {1}", - FormatOptionsContext(msg), String.Join(", ", legalInputs)); - Warn(msg); - }*/ - - // Make sure we convert all the incorrect case params to the ones set by the module spec - foreach (string key in caseUnsupportedParameters) - { - string correctKey = legalInputs[legalInputs.FindIndex(s => s.Equals(key, StringComparison.OrdinalIgnoreCase))]; - object value = param[key]; - param.Remove(key); - param.Add(correctKey, value); - } - } - - private void CheckMutuallyExclusive(IDictionary param, IList mutuallyExclusive) - { - if (mutuallyExclusive == null) - return; - - foreach (object check in mutuallyExclusive) - { - List mutualCheck = ((IList)check).Cast().ToList(); - int count = 0; - foreach (string entry in mutualCheck) - if (param.Contains(entry)) - count++; - - if (count > 1) - { - string msg = String.Format("parameters are mutually exclusive: {0}", String.Join(", ", mutualCheck)); - FailJson(FormatOptionsContext(msg)); - } - } - } - - private void CheckRequiredArguments(IDictionary spec, IDictionary param) - { - List missing = new List(); - foreach (DictionaryEntry entry in spec) - { - string k = (string)entry.Key; - Hashtable v = (Hashtable)entry.Value; - - // set defaults for values not already set - object defaultValue = v["default"]; - if (defaultValue != null && !param.Contains(k)) - param[k] = defaultValue; - - // check required arguments - bool required = (bool)v["required"]; - if (required && !param.Contains(k)) - missing.Add(k); - } - if (missing.Count > 0) - { - string msg = String.Format("missing required arguments: {0}", String.Join(", ", missing)); - FailJson(FormatOptionsContext(msg)); - } - } - - private void CheckRequiredTogether(IDictionary param, IList requiredTogether) - { - if (requiredTogether == null) - return; - - foreach (object check in requiredTogether) - { - List requiredCheck = ((IList)check).Cast().ToList(); - List found = new List(); - foreach (string field in requiredCheck) - if (param.Contains(field)) - found.Add(true); - else - found.Add(false); - - if (found.Contains(true) && found.Contains(false)) - { - string msg = String.Format("parameters are required together: {0}", String.Join(", ", requiredCheck)); - FailJson(FormatOptionsContext(msg)); - } - } - } - - private void CheckRequiredOneOf(IDictionary param, IList requiredOneOf) - { - if (requiredOneOf == null) - return; - - foreach (object check in requiredOneOf) - { - List requiredCheck = ((IList)check).Cast().ToList(); - int count = 0; - foreach (string field in requiredCheck) - if (param.Contains(field)) - count++; - - if (count == 0) - { - string msg = String.Format("one of the following is required: {0}", String.Join(", ", requiredCheck)); - FailJson(FormatOptionsContext(msg)); - } - } - } - - private void CheckRequiredIf(IDictionary param, IList requiredIf) - { - if (requiredIf == null) - return; - - foreach (object check in requiredIf) - { - IList requiredCheck = (IList)check; - List missing = new List(); - List missingFields = new List(); - int maxMissingCount = 1; - bool oneRequired = false; - - if (requiredCheck.Count < 3 && requiredCheck.Count < 4) - FailJson(String.Format("internal error: invalid required_if value count of {0}, expecting 3 or 4 entries", requiredCheck.Count)); - else if (requiredCheck.Count == 4) - oneRequired = (bool)requiredCheck[3]; - - string key = (string)requiredCheck[0]; - object val = requiredCheck[1]; - IList requirements = (IList)requiredCheck[2]; - - if (ParseStr(param[key]) != ParseStr(val)) - continue; - - string term = "all"; - if (oneRequired) - { - maxMissingCount = requirements.Count; - term = "any"; - } - - foreach (string required in requirements.Cast()) - if (!param.Contains(required)) - missing.Add(required); - - if (missing.Count >= maxMissingCount) - { - string msg = String.Format("{0} is {1} but {2} of the following are missing: {3}", - key, val.ToString(), term, String.Join(", ", missing)); - FailJson(FormatOptionsContext(msg)); - } - } - } - - private void CheckRequiredBy(IDictionary param, IDictionary requiredBy) - { - foreach (DictionaryEntry entry in requiredBy) - { - string key = (string)entry.Key; - if (!param.Contains(key)) - continue; - - List missing = new List(); - List requires = ParseList(entry.Value).Cast().ToList(); - foreach (string required in requires) - if (!param.Contains(required)) - missing.Add(required); - - if (missing.Count > 0) - { - string msg = String.Format("missing parameter(s) required by '{0}': {1}", key, String.Join(", ", missing)); - FailJson(FormatOptionsContext(msg)); - } - } - } - - private void CheckSubOption(IDictionary param, string key, IDictionary spec) - { - object value = param[key]; - - string type; - if (spec["type"].GetType() == typeof(string)) - type = (string)spec["type"]; - else - type = "delegate"; - - string elements = null; - Delegate typeConverter = null; - if (spec["elements"] != null && spec["elements"].GetType() == typeof(string)) - { - elements = (string)spec["elements"]; - typeConverter = optionTypes[elements]; - } - else if (spec["elements"] != null) - { - elements = "delegate"; - typeConverter = (Delegate)spec["elements"]; - } - - if (!(type == "dict" || (type == "list" && elements != null))) - // either not a dict, or list with the elements set, so continue - return; - else if (type == "list") - { - // cast each list element to the type specified - if (value == null) - return; - - List newValue = new List(); - foreach (object element in (List)value) - { - if (elements == "dict") - newValue.Add(ParseSubSpec(spec, element, key)); - else - { - try - { - object newElement = typeConverter.DynamicInvoke(element); - newValue.Add(newElement); - } - catch (Exception e) - { - string msg = String.Format("argument for list entry {0} is of type {1} and we were unable to convert to {2}: {3}", - key, element.GetType(), elements, e.Message); - FailJson(FormatOptionsContext(msg)); - } - } - } - - param[key] = newValue; - } - else - param[key] = ParseSubSpec(spec, value, key); - } - - private object ParseSubSpec(IDictionary spec, object value, string context) - { - bool applyDefaults = (bool)spec["apply_defaults"]; - - // set entry to an empty dict if apply_defaults is set - IDictionary optionsSpec = (IDictionary)spec["options"]; - if (applyDefaults && optionsSpec.Keys.Count > 0 && value == null) - value = new Dictionary(); - else if (optionsSpec.Keys.Count == 0 || value == null) - return value; - - optionsContext.Add(context); - Dictionary newValue = (Dictionary)ParseDict(value); - Dictionary aliases = GetAliases(spec, newValue); - SetNoLogValues(spec, newValue); - - List subLegalInputs = optionsSpec.Keys.Cast().ToList(); - subLegalInputs.AddRange(aliases.Keys.Cast().ToList()); - - CheckArguments(spec, newValue, subLegalInputs); - optionsContext.RemoveAt(optionsContext.Count - 1); - return newValue; - } - - private string GetFormattedResults(Dictionary result) - { - if (!result.ContainsKey("invocation")) - result["invocation"] = new Dictionary() { { "module_args", RemoveNoLogValues(Params, noLogValues) } }; - - if (warnings.Count > 0) - result["warnings"] = warnings; - - if (deprecations.Count > 0) - result["deprecations"] = deprecations; - - if (Diff.Count > 0 && DiffMode) - result["diff"] = Diff; - - return ToJson(result); - } - - private string FormatLogData(object data, int indentLevel) - { - if (data == null) - return "$null"; - - string msg = ""; - if (data is IList) - { - string newMsg = ""; - foreach (object value in (IList)data) - { - string entryValue = FormatLogData(value, indentLevel + 2); - newMsg += String.Format("\r\n{0}- {1}", new String(' ', indentLevel), entryValue); - } - msg += newMsg; - } - else if (data is IDictionary) - { - bool start = true; - foreach (DictionaryEntry entry in (IDictionary)data) - { - string newMsg = FormatLogData(entry.Value, indentLevel + 2); - if (!start) - msg += String.Format("\r\n{0}", new String(' ', indentLevel)); - msg += String.Format("{0}: {1}", (string)entry.Key, newMsg); - start = false; - } - } - else - msg = (string)RemoveNoLogValues(ParseStr(data), noLogValues); - - return msg; - } - - private object RemoveNoLogValues(object value, HashSet noLogStrings) - { - Queue> deferredRemovals = new Queue>(); - object newValue = RemoveValueConditions(value, noLogStrings, deferredRemovals); - - while (deferredRemovals.Count > 0) - { - Tuple data = deferredRemovals.Dequeue(); - object oldData = data.Item1; - object newData = data.Item2; - - if (oldData is IDictionary) - { - foreach (DictionaryEntry entry in (IDictionary)oldData) - { - object newElement = RemoveValueConditions(entry.Value, noLogStrings, deferredRemovals); - ((IDictionary)newData).Add((string)entry.Key, newElement); - } - } - else - { - foreach (object element in (IList)oldData) - { - object newElement = RemoveValueConditions(element, noLogStrings, deferredRemovals); - ((IList)newData).Add(newElement); - } - } - } - - return newValue; - } - - private object RemoveValueConditions(object value, HashSet noLogStrings, Queue> deferredRemovals) - { - if (value == null) - return value; - - Type valueType = value.GetType(); - HashSet numericTypes = new HashSet - { - typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), - typeof(long), typeof(ulong), typeof(decimal), typeof(double), typeof(float) - }; - - if (numericTypes.Contains(valueType) || valueType == typeof(bool)) - { - string valueString = ParseStr(value); - if (noLogStrings.Contains(valueString)) - return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"; - foreach (string omitMe in noLogStrings) - if (valueString.Contains(omitMe)) - return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"; - } - else if (valueType == typeof(DateTime)) - value = ((DateTime)value).ToString("o"); - else if (value is IList) - { - List newValue = new List(); - deferredRemovals.Enqueue(new Tuple((IList)value, newValue)); - value = newValue; - } - else if (value is IDictionary) - { - Hashtable newValue = new Hashtable(); - deferredRemovals.Enqueue(new Tuple((IDictionary)value, newValue)); - value = newValue; - } - else - { - string stringValue = value.ToString(); - if (noLogStrings.Contains(stringValue)) - return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"; - foreach (string omitMe in noLogStrings) - if (stringValue.Contains(omitMe)) - return (stringValue).Replace(omitMe, "********"); - value = stringValue; - } - return value; - } - - private void CleanupFiles(object s, EventArgs ev) - { - foreach (string path in cleanupFiles) - { - if (File.Exists(path)) - File.Delete(path); - else if (Directory.Exists(path)) - Directory.Delete(path, true); - } - cleanupFiles = new List(); - } - - private string FormatOptionsContext(string msg, string prefix = " ") - { - if (optionsContext.Count > 0) - msg += String.Format("{0}found in {1}", prefix, String.Join(" -> ", optionsContext)); - return msg; - } - - [DllImport("kernel32.dll")] - private static extern IntPtr GetConsoleWindow(); - - private static void ExitModule(int rc) - { - // When running in a Runspace Environment.Exit will kill the entire - // process which is not what we want, detect if we are in a - // Runspace and call a ScriptBlock with exit instead. - if (Runspace.DefaultRunspace != null) - ScriptBlock.Create("Set-Variable -Name LASTEXITCODE -Value $args[0] -Scope Global; exit $args[0]").Invoke(rc); - else - { - // Used for local debugging in Visual Studio - if (System.Diagnostics.Debugger.IsAttached) - { - Console.WriteLine("Press enter to continue..."); - Console.ReadLine(); - } - Environment.Exit(rc); - } - } - - private static void WriteLineModule(string line) - { - Console.WriteLine(line); - } - } -} diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.ModuleUtils.AddType.psm1 b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.ModuleUtils.AddType.psm1 deleted file mode 100644 index 673070263..000000000 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/Ansible.ModuleUtils.AddType.psm1 +++ /dev/null @@ -1,397 +0,0 @@ -# Copyright (c) 2018 Ansible Project -# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) - -Function Add-CSharpType { - <# - .SYNOPSIS - Compiles one or more C# scripts similar to Add-Type. This exposes - more configuration options that are useable within Ansible and it - also allows multiple C# sources to be compiled together. - - .PARAMETER References - [String[]] A collection of C# scripts to compile together. - - .PARAMETER IgnoreWarnings - [Switch] Whether to compile code that contains compiler warnings, by - default warnings will cause a compiler error. - - .PARAMETER PassThru - [Switch] Whether to return the loaded Assembly - - .PARAMETER AnsibleModule - [Ansible.Basic.AnsibleModule] used to derive the TempPath and Debug values. - TempPath is set to the Tmpdir property of the class - IncludeDebugInfo is set when the Ansible verbosity is >= 3 - - .PARAMETER TempPath - [String] The temporary directory in which the dynamic assembly is - compiled to. This file is deleted once compilation is complete. - Cannot be used when AnsibleModule is set. This is a no-op when - running on PSCore. - - .PARAMETER IncludeDebugInfo - [Switch] Whether to include debug information in the compiled - assembly. Cannot be used when AnsibleModule is set. This is a no-op - when running on PSCore. - - .PARAMETER CompileSymbols - [String[]] A list of symbols to be defined during compile time. These are - added to the existing symbols, 'CORECLR', 'WINDOWS', 'UNIX' that are set - conditionalls in this cmdlet. - - .NOTES - The following features were added to control the compiling options from the - code itself. - - * Predefined compiler SYMBOLS - - * CORECLR - Added when running on PowerShell Core. - * WINDOWS - Added when running on Windows. - * UNIX - Added when running on non-Windows. - * X86 - Added when running on a 32-bit process (Ansible 2.10+) - * AMD64 - Added when running on a 64-bit process (Ansible 2.10+) - - * Ignore compiler warnings inline with the following comment inline - - //NoWarn -Name [-CLR Core|Framework] - - * Specify custom assembly references inline - - //AssemblyReference -Name Dll.Location.dll [-CLR Core|Framework] - - # Added in Ansible 2.10 - //AssemblyReference -Type System.Type.Name [-CLR Core|Framework] - - * Create automatic type accelerators to simplify long namespace names (Ansible 2.9+) - - //TypeAccelerator -Name -TypeName - #> - param( - [Parameter(Mandatory = $true)][AllowEmptyCollection()][String[]]$References, - [Switch]$IgnoreWarnings, - [Switch]$PassThru, - [Parameter(Mandatory = $true, ParameterSetName = "Module")][Object]$AnsibleModule, - [Parameter(ParameterSetName = "Manual")][String]$TempPath = $env:TMP, - [Parameter(ParameterSetName = "Manual")][Switch]$IncludeDebugInfo, - [String[]]$CompileSymbols = @() - ) - if ($null -eq $References -or $References.Length -eq 0) { - return - } - - # define special symbols CORECLR, WINDOWS, UNIX if required - # the Is* variables are defined on PSCore, if absent we assume an - # older version of PowerShell under .NET Framework and Windows - $defined_symbols = [System.Collections.ArrayList]$CompileSymbols - - if ([System.IntPtr]::Size -eq 4) { - $defined_symbols.Add('X86') > $null - } - else { - $defined_symbols.Add('AMD64') > $null - } - - $is_coreclr = Get-Variable -Name IsCoreCLR -ErrorAction SilentlyContinue - if ($null -ne $is_coreclr) { - if ($is_coreclr.Value) { - $defined_symbols.Add("CORECLR") > $null - } - } - $is_windows = Get-Variable -Name IsWindows -ErrorAction SilentlyContinue - if ($null -ne $is_windows) { - if ($is_windows.Value) { - $defined_symbols.Add("WINDOWS") > $null - } - else { - $defined_symbols.Add("UNIX") > $null - } - } - else { - $defined_symbols.Add("WINDOWS") > $null - } - - # Store any TypeAccelerators shortcuts the util wants us to set - $type_accelerators = [System.Collections.Generic.List`1[Hashtable]]@() - - # pattern used to find referenced assemblies in the code - $assembly_pattern = [Regex]"//\s*AssemblyReference\s+-(?(Name)|(Type))\s+(?[\w.]*)(\s+-CLR\s+(?Core|Framework))?" - $no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?[\w\d]*)(\s+-CLR\s+(?Core|Framework))?" - $type_pattern = [Regex]"//\s*TypeAccelerator\s+-Name\s+(?[\w.]*)\s+-TypeName\s+(?[\w.]*)" - - # PSCore vs PSDesktop use different methods to compile the code, - # PSCore uses Roslyn and can compile the code purely in memory - # without touching the disk while PSDesktop uses CodeDom and csc.exe - # to compile the code. We branch out here and run each - # distribution's method to add our C# code. - if ($is_coreclr) { - # compile the code using Roslyn on PSCore - - # Include the default assemblies using the logic in Add-Type - # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs - $assemblies = [System.Collections.Generic.HashSet`1[Microsoft.CodeAnalysis.MetadataReference]]@( - [Microsoft.CodeAnalysis.CompilationReference]::CreateFromFile(([System.Reflection.Assembly]::GetAssembly([PSObject])).Location) - ) - $netcore_app_ref_folder = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName([PSObject].Assembly.Location), "ref") - $lib_assembly_location = [System.IO.Path]::GetDirectoryName([object].Assembly.Location) - foreach ($file in [System.IO.Directory]::EnumerateFiles($netcore_app_ref_folder, "*.dll", [System.IO.SearchOption]::TopDirectoryOnly)) { - $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($file)) > $null - } - - # loop through the references, parse as a SyntaxTree and get - # referenced assemblies - $ignore_warnings = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[String], [Microsoft.CodeAnalysis.ReportDiagnostic]]' - $parse_options = ([Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default).WithPreprocessorSymbols($defined_symbols) - $syntax_trees = [System.Collections.Generic.List`1[Microsoft.CodeAnalysis.SyntaxTree]]@() - foreach ($reference in $References) { - # scan through code and add any assemblies that match - # //AssemblyReference -Name ... [-CLR Core] - # //NoWarn -Name ... [-CLR Core] - # //TypeAccelerator -Name ... -TypeName ... - $assembly_matches = $assembly_pattern.Matches($reference) - foreach ($match in $assembly_matches) { - $clr = $match.Groups["CLR"].Value - if ($clr -and $clr -ne "Core") { - continue - } - - $parameter_type = $match.Groups["Parameter"].Value - $assembly_path = $match.Groups["Name"].Value - if ($parameter_type -eq "Type") { - $assembly_path = ([Type]$assembly_path).Assembly.Location - } - else { - if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) { - $assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path - } - } - $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null - } - $warn_matches = $no_warn_pattern.Matches($reference) - foreach ($match in $warn_matches) { - $clr = $match.Groups["CLR"].Value - if ($clr -and $clr -ne "Core") { - continue - } - $ignore_warnings.Add($match.Groups["Name"], [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress) - } - $syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null - - $type_matches = $type_pattern.Matches($reference) - foreach ($match in $type_matches) { - $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value }) - } - } - - # Release seems to contain the correct line numbers compared to - # debug,may need to keep a closer eye on this in the future - $compiler_options = (New-Object -TypeName Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions -ArgumentList @( - [Microsoft.CodeAnalysis.OutputKind]::DynamicallyLinkedLibrary - )).WithOptimizationLevel([Microsoft.CodeAnalysis.OptimizationLevel]::Release) - - # set warnings to error out if IgnoreWarnings is not set - if (-not $IgnoreWarnings.IsPresent) { - $compiler_options = $compiler_options.WithGeneralDiagnosticOption([Microsoft.CodeAnalysis.ReportDiagnostic]::Error) - $compiler_options = $compiler_options.WithSpecificDiagnosticOptions($ignore_warnings) - } - - # create compilation object - $compilation = [Microsoft.CodeAnalysis.CSharp.CSharpCompilation]::Create( - [System.Guid]::NewGuid().ToString(), - $syntax_trees, - $assemblies, - $compiler_options - ) - - # Load the compiled code and pdb info, we do this so we can - # include line number in a stracktrace - $code_ms = New-Object -TypeName System.IO.MemoryStream - $pdb_ms = New-Object -TypeName System.IO.MemoryStream - try { - $emit_result = $compilation.Emit($code_ms, $pdb_ms) - if (-not $emit_result.Success) { - $errors = [System.Collections.ArrayList]@() - - foreach ($e in $emit_result.Diagnostics) { - # builds the error msg, based on logic in Add-Type - # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs#L1239 - if ($null -eq $e.Location.SourceTree) { - $errors.Add($e.ToString()) > $null - continue - } - - $cancel_token = New-Object -TypeName System.Threading.CancellationToken -ArgumentList $false - $text_lines = $e.Location.SourceTree.GetText($cancel_token).Lines - $line_span = $e.Location.GetLineSpan() - - $diagnostic_message = $e.ToString() - $error_line_string = $text_lines[$line_span.StartLinePosition.Line].ToString() - $error_position = $line_span.StartLinePosition.Character - - $sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($diagnostic_message.Length + $error_line_string.Length * 2 + 4) - $sb.AppendLine($diagnostic_message) - $sb.AppendLine($error_line_string) - - for ($i = 0; $i -lt $error_line_string.Length; $i++) { - if ([System.Char]::IsWhiteSpace($error_line_string[$i])) { - continue - } - $sb.Append($error_line_string, 0, $i) - $sb.Append(' ', [Math]::Max(0, $error_position - $i)) - $sb.Append("^") - break - } - - $errors.Add($sb.ToString()) > $null - } - - throw [InvalidOperationException]"Failed to compile C# code:`r`n$($errors -join "`r`n")" - } - - $code_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null - $pdb_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null - $compiled_assembly = [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($code_ms, $pdb_ms) - } - finally { - $code_ms.Close() - $pdb_ms.Close() - } - } - else { - # compile the code using CodeDom on PSDesktop - - # configure compile options based on input - if ($PSCmdlet.ParameterSetName -eq "Module") { - $temp_path = $AnsibleModule.Tmpdir - $include_debug = $AnsibleModule.Verbosity -ge 3 - } - else { - $temp_path = $TempPath - $include_debug = $IncludeDebugInfo.IsPresent - } - $compiler_options = [System.Collections.ArrayList]@("/optimize") - if ($defined_symbols.Count -gt 0) { - $compiler_options.Add("/define:" + ([String]::Join(";", $defined_symbols.ToArray()))) > $null - } - - $compile_parameters = New-Object -TypeName System.CodeDom.Compiler.CompilerParameters - $compile_parameters.GenerateExecutable = $false - $compile_parameters.GenerateInMemory = $true - $compile_parameters.TreatWarningsAsErrors = (-not $IgnoreWarnings.IsPresent) - $compile_parameters.IncludeDebugInformation = $include_debug - $compile_parameters.TempFiles = (New-Object -TypeName System.CodeDom.Compiler.TempFileCollection -ArgumentList $temp_path, $false) - - # Add-Type automatically references System.dll, System.Core.dll, - # and System.Management.Automation.dll which we replicate here - $assemblies = [System.Collections.Generic.HashSet`1[String]]@( - "System.dll", - "System.Core.dll", - ([System.Reflection.Assembly]::GetAssembly([PSObject])).Location - ) - - # create a code snippet for each reference and check if we need - # to reference any extra assemblies - $ignore_warnings = [System.Collections.ArrayList]@() - $compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@() - foreach ($reference in $References) { - # scan through code and add any assemblies that match - # //AssemblyReference -Name ... [-CLR Framework] - # //NoWarn -Name ... [-CLR Framework] - # //TypeAccelerator -Name ... -TypeName ... - $assembly_matches = $assembly_pattern.Matches($reference) - foreach ($match in $assembly_matches) { - $clr = $match.Groups["CLR"].Value - if ($clr -and $clr -ne "Framework") { - continue - } - - $parameter_type = $match.Groups["Parameter"].Value - $assembly_path = $match.Groups["Name"].Value - if ($parameter_type -eq "Type") { - $assembly_path = ([Type]$assembly_path).Assembly.Location - } - $assemblies.Add($assembly_path) > $null - } - $warn_matches = $no_warn_pattern.Matches($reference) - foreach ($match in $warn_matches) { - $clr = $match.Groups["CLR"].Value - if ($clr -and $clr -ne "Framework") { - continue - } - $warning_id = $match.Groups["Name"].Value - # /nowarn should only contain the numeric part - if ($warning_id.StartsWith("CS")) { - $warning_id = $warning_id.Substring(2) - } - $ignore_warnings.Add($warning_id) > $null - } - $compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null - - $type_matches = $type_pattern.Matches($reference) - foreach ($match in $type_matches) { - $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value }) - } - } - if ($ignore_warnings.Count -gt 0) { - $compiler_options.Add("/nowarn:" + ([String]::Join(",", $ignore_warnings.ToArray()))) > $null - } - $compile_parameters.ReferencedAssemblies.AddRange($assemblies) - $compile_parameters.CompilerOptions = [String]::Join(" ", $compiler_options.ToArray()) - - # compile the code together and check for errors - $provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider - - # This calls csc.exe which can take compiler options from environment variables. Currently these env vars - # are known to have problems so they are unset: - # LIB - additional library paths will fail the compilation if they are invalid - $originalEnv = @{} - try { - 'LIB' | ForEach-Object -Process { - $value = Get-Item -LiteralPath "Env:\$_" -ErrorAction SilentlyContinue - if ($value) { - $originalEnv[$_] = $value - Remove-Item -LiteralPath "Env:\$_" - } - } - - $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units) - } - finally { - foreach ($kvp in $originalEnv.GetEnumerator()) { - [System.Environment]::SetEnvironmentVariable($kvp.Key, $kvp.Value, "Process") - } - } - - if ($compile.Errors.HasErrors) { - $msg = "Failed to compile C# code: " - foreach ($e in $compile.Errors) { - $msg += "`r`n" + $e.ToString() - } - throw [InvalidOperationException]$msg - } - $compiled_assembly = $compile.CompiledAssembly - } - - $type_accelerator = [PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators") - foreach ($accelerator in $type_accelerators) { - $type_name = $accelerator.TypeName - $found = $false - - foreach ($assembly_type in $compiled_assembly.GetTypes()) { - if ($assembly_type.Name -eq $type_name) { - $type_accelerator::Add($accelerator.Name, $assembly_type) - $found = $true - break - } - } - if (-not $found) { - throw "Failed to find compiled class '$type_name' for custom TypeAccelerator." - } - } - - # return the compiled assembly if PassThru is set. - if ($PassThru) { - return $compiled_assembly - } -} - -Export-ModuleMember -Function Add-CSharpType diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.Basic.cs b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.Basic.cs new file mode 100644 index 000000000..c68281ef1 --- /dev/null +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.Basic.cs @@ -0,0 +1,1489 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Principal; +#if CORECLR +using Newtonsoft.Json; +#else +using System.Web.Script.Serialization; +#endif + +// Newtonsoft.Json may reference a different System.Runtime version (6.x) than loaded by PowerShell 7.3 (7.x). +// Ignore CS1701 so the code can be compiled when warnings are reported as errors. +//NoWarn -Name CS1701 -CLR Core + +// System.Diagnostics.EventLog.dll reference different versioned dlls that are +// loaded in PSCore, ignore CS1702 so the code will ignore this warning +//NoWarn -Name CS1702 -CLR Core + +//AssemblyReference -Type Newtonsoft.Json.JsonConvert -CLR Core +//AssemblyReference -Type System.Diagnostics.EventLog -CLR Core +//AssemblyReference -Type System.Security.AccessControl.NativeObjectSecurity -CLR Core +//AssemblyReference -Type System.Security.AccessControl.DirectorySecurity -CLR Core +//AssemblyReference -Type System.Security.Principal.IdentityReference -CLR Core + +//AssemblyReference -Name System.Web.Extensions.dll -CLR Framework + +namespace Ansible.Basic +{ + public class AnsibleModule + { + public delegate void ExitHandler(int rc); + public static ExitHandler Exit = new ExitHandler(ExitModule); + + public delegate void WriteLineHandler(string line); + public static WriteLineHandler WriteLine = new WriteLineHandler(WriteLineModule); + + public static bool _DebugArgSpec = false; + + private static List BOOLEANS_TRUE = new List() { "y", "yes", "on", "1", "true", "t", "1.0" }; + private static List BOOLEANS_FALSE = new List() { "n", "no", "off", "0", "false", "f", "0.0" }; + + private string remoteTmp = Path.GetTempPath(); + private string tmpdir = null; + private HashSet noLogValues = new HashSet(); + private List optionsContext = new List(); + private List warnings = new List(); + private List> deprecations = new List>(); + private List cleanupFiles = new List(); + + private Dictionary passVars = new Dictionary() + { + // null values means no mapping, not used in Ansible.Basic.AnsibleModule + { "check_mode", "CheckMode" }, + { "debug", "DebugMode" }, + { "diff", "DiffMode" }, + { "keep_remote_files", "KeepRemoteFiles" }, + { "module_name", "ModuleName" }, + { "no_log", "NoLog" }, + { "remote_tmp", "remoteTmp" }, + { "selinux_special_fs", null }, + { "shell_executable", null }, + { "socket", null }, + { "string_conversion_action", null }, + { "syslog_facility", null }, + { "tmpdir", "tmpdir" }, + { "verbosity", "Verbosity" }, + { "version", "AnsibleVersion" }, + }; + private List passBools = new List() { "check_mode", "debug", "diff", "keep_remote_files", "no_log" }; + private List passInts = new List() { "verbosity" }; + private Dictionary> specDefaults = new Dictionary>() + { + // key - (default, type) - null is freeform + { "apply_defaults", new List() { false, typeof(bool) } }, + { "aliases", new List() { typeof(List), typeof(List) } }, + { "choices", new List() { typeof(List), typeof(List) } }, + { "default", new List() { null, null } }, + { "deprecated_aliases", new List() { typeof(List), typeof(List) } }, + { "elements", new List() { null, null } }, + { "mutually_exclusive", new List() { typeof(List>), typeof(List) } }, + { "no_log", new List() { false, typeof(bool) } }, + { "options", new List() { typeof(Hashtable), typeof(Hashtable) } }, + { "removed_in_version", new List() { null, typeof(string) } }, + { "removed_at_date", new List() { null, typeof(DateTime) } }, + { "removed_from_collection", new List() { null, typeof(string) } }, + { "required", new List() { false, typeof(bool) } }, + { "required_by", new List() { typeof(Hashtable), typeof(Hashtable) } }, + { "required_if", new List() { typeof(List>), typeof(List) } }, + { "required_one_of", new List() { typeof(List>), typeof(List) } }, + { "required_together", new List() { typeof(List>), typeof(List) } }, + { "supports_check_mode", new List() { false, typeof(bool) } }, + { "type", new List() { "str", null } }, + }; + private Dictionary optionTypes = new Dictionary() + { + { "bool", new Func(ParseBool) }, + { "dict", new Func>(ParseDict) }, + { "float", new Func(ParseFloat) }, + { "int", new Func(ParseInt) }, + { "json", new Func(ParseJson) }, + { "list", new Func>(ParseList) }, + { "path", new Func(ParsePath) }, + { "raw", new Func(ParseRaw) }, + { "sid", new Func(ParseSid) }, + { "str", new Func(ParseStr) }, + }; + + public Dictionary Diff = new Dictionary(); + public IDictionary Params = null; + public Dictionary Result = new Dictionary() { { "changed", false } }; + + public bool CheckMode { get; private set; } + public bool DebugMode { get; private set; } + public bool DiffMode { get; private set; } + public bool KeepRemoteFiles { get; private set; } + public string ModuleName { get; private set; } + public bool NoLog { get; private set; } + public int Verbosity { get; private set; } + public string AnsibleVersion { get; private set; } + + public string Tmpdir + { + get + { + if (tmpdir == null) + { +#if WINDOWS + SecurityIdentifier user = WindowsIdentity.GetCurrent().User; + DirectorySecurity dirSecurity = new DirectorySecurity(); + dirSecurity.SetOwner(user); + dirSecurity.SetAccessRuleProtection(true, false); // disable inheritance rules + FileSystemAccessRule ace = new FileSystemAccessRule(user, FileSystemRights.FullControl, + InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, + PropagationFlags.None, AccessControlType.Allow); + dirSecurity.AddAccessRule(ace); + + string baseDir = Path.GetFullPath(Environment.ExpandEnvironmentVariables(remoteTmp)); + if (!Directory.Exists(baseDir)) + { + string failedMsg = null; + try + { +#if CORECLR + DirectoryInfo createdDir = Directory.CreateDirectory(baseDir); + FileSystemAclExtensions.SetAccessControl(createdDir, dirSecurity); +#else + Directory.CreateDirectory(baseDir, dirSecurity); +#endif + } + catch (Exception e) + { + failedMsg = String.Format("Failed to create base tmpdir '{0}': {1}", baseDir, e.Message); + } + + if (failedMsg != null) + { + string envTmp = Path.GetTempPath(); + Warn(String.Format("Unable to use '{0}' as temporary directory, falling back to system tmp '{1}': {2}", baseDir, envTmp, failedMsg)); + baseDir = envTmp; + } + else + { + NTAccount currentUser = (NTAccount)user.Translate(typeof(NTAccount)); + string warnMsg = String.Format("Module remote_tmp {0} did not exist and was created with FullControl to {1}, ", baseDir, currentUser.ToString()); + warnMsg += "this may cause issues when running as another user. To avoid this, create the remote_tmp dir with the correct permissions manually"; + Warn(warnMsg); + } + } + + string dateTime = DateTime.Now.ToFileTime().ToString(); + string dirName = String.Format("ansible-moduletmp-{0}-{1}", dateTime, new Random().Next(0, int.MaxValue)); + string newTmpdir = Path.Combine(baseDir, dirName); +#if CORECLR + DirectoryInfo tmpdirInfo = Directory.CreateDirectory(newTmpdir); + FileSystemAclExtensions.SetAccessControl(tmpdirInfo, dirSecurity); +#else + Directory.CreateDirectory(newTmpdir, dirSecurity); +#endif + tmpdir = newTmpdir; + + if (!KeepRemoteFiles) + cleanupFiles.Add(tmpdir); +#else + throw new NotImplementedException("Tmpdir is only supported on Windows"); +#endif + } + return tmpdir; + } + } + + public AnsibleModule(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null) + { + // NoLog is not set yet, we cannot rely on FailJson to sanitize the output + // Do the minimum amount to get this running before we actually parse the params + Dictionary aliases = new Dictionary(); + try + { + ValidateArgumentSpec(argumentSpec); + + // Merge the fragments if present into the main arg spec. + if (fragments != null) + { + foreach (IDictionary fragment in fragments) + { + ValidateArgumentSpec(fragment); + MergeFragmentSpec(argumentSpec, fragment); + } + } + + // Used by ansible-test to retrieve the module argument spec, not designed for public use. + if (_DebugArgSpec) + { + // Cannot call exit here because it will be caught with the catch (Exception e) below. Instead + // just throw a new exception with a specific message and the exception block will handle it. + ScriptBlock.Create("Set-Variable -Name ansibleTestArgSpec -Value $args[0] -Scope Global" + ).Invoke(argumentSpec); + throw new Exception("ansible-test validate-modules check"); + } + + // Now make sure all the metadata keys are set to their defaults, this must be done after we've + // potentially output the arg spec for ansible-test. + SetArgumentSpecDefaults(argumentSpec); + + Params = GetParams(args); + aliases = GetAliases(argumentSpec, Params); + SetNoLogValues(argumentSpec, Params); + } + catch (Exception e) + { + if (e.Message == "ansible-test validate-modules check") + Exit(0); + + Dictionary result = new Dictionary + { + { "failed", true }, + { "msg", String.Format("internal error: {0}", e.Message) }, + { "exception", e.ToString() } + }; + WriteLine(ToJson(result)); + Exit(1); + } + + // Initialise public properties to the defaults before we parse the actual inputs + CheckMode = false; + DebugMode = false; + DiffMode = false; + KeepRemoteFiles = false; + ModuleName = "undefined win module"; + NoLog = (bool)argumentSpec["no_log"]; + Verbosity = 0; + AppDomain.CurrentDomain.ProcessExit += CleanupFiles; + + List legalInputs = passVars.Keys.Select(v => "_ansible_" + v).ToList(); + legalInputs.AddRange(((IDictionary)argumentSpec["options"]).Keys.Cast().ToList()); + legalInputs.AddRange(aliases.Keys.Cast().ToList()); + CheckArguments(argumentSpec, Params, legalInputs); + + // Set a Ansible friendly invocation value in the result object + Dictionary invocation = new Dictionary() { { "module_args", Params } }; + Result["invocation"] = RemoveNoLogValues(invocation, noLogValues); + + if (!NoLog) + LogEvent(String.Format("Invoked with:\r\n {0}", FormatLogData(Params, 2)), sanitise: false); + } + + public static AnsibleModule Create(string[] args, IDictionary argumentSpec, IDictionary[] fragments = null) + { + return new AnsibleModule(args, argumentSpec, fragments); + } + + public void Debug(string message) + { + if (DebugMode) + LogEvent(String.Format("[DEBUG] {0}", message)); + } + + public void Deprecate(string message, string version) + { + Deprecate(message, version, null); + } + + public void Deprecate(string message, string version, string collectionName) + { + deprecations.Add(new Dictionary() { + { "msg", message }, { "version", version }, { "collection_name", collectionName } }); + LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, version)); + } + + public void Deprecate(string message, DateTime date) + { + Deprecate(message, date, null); + } + + public void Deprecate(string message, DateTime date, string collectionName) + { + string isoDate = date.ToString("yyyy-MM-dd"); + deprecations.Add(new Dictionary() { + { "msg", message }, { "date", isoDate }, { "collection_name", collectionName } }); + LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, isoDate)); + } + + public void ExitJson() + { + WriteLine(GetFormattedResults(Result)); + CleanupFiles(null, null); + Exit(0); + } + + public void FailJson(string message) { FailJson(message, null, null); } + public void FailJson(string message, ErrorRecord psErrorRecord) { FailJson(message, psErrorRecord, null); } + public void FailJson(string message, Exception exception) { FailJson(message, null, exception); } + private void FailJson(string message, ErrorRecord psErrorRecord, Exception exception) + { + Result["failed"] = true; + Result["msg"] = RemoveNoLogValues(message, noLogValues); + + + if (!Result.ContainsKey("exception") && (Verbosity > 2 || DebugMode)) + { + if (psErrorRecord != null) + { + string traceback = String.Format("{0}\r\n{1}", psErrorRecord.ToString(), psErrorRecord.InvocationInfo.PositionMessage); + traceback += String.Format("\r\n + CategoryInfo : {0}", psErrorRecord.CategoryInfo.ToString()); + traceback += String.Format("\r\n + FullyQualifiedErrorId : {0}", psErrorRecord.FullyQualifiedErrorId.ToString()); + traceback += String.Format("\r\n\r\nScriptStackTrace:\r\n{0}", psErrorRecord.ScriptStackTrace); + Result["exception"] = traceback; + } + else if (exception != null) + Result["exception"] = exception.ToString(); + } + + WriteLine(GetFormattedResults(Result)); + CleanupFiles(null, null); + Exit(1); + } + + public void LogEvent(string message, EventLogEntryType logEntryType = EventLogEntryType.Information, bool sanitise = true) + { + if (NoLog) + return; + +#if WINDOWS + string logSource = "Ansible"; + bool logSourceExists = false; + try + { + logSourceExists = EventLog.SourceExists(logSource); + } + catch (System.Security.SecurityException) { } // non admin users may not have permission + + if (!logSourceExists) + { + try + { + EventLog.CreateEventSource(logSource, "Application"); + } + catch (System.Security.SecurityException) + { + // Cannot call Warn as that calls LogEvent and we get stuck in a loop + warnings.Add(String.Format("Access error when creating EventLog source {0}, logging to the Application source instead", logSource)); + logSource = "Application"; + } + } + if (sanitise) + message = (string)RemoveNoLogValues(message, noLogValues); + message = String.Format("{0} - {1}", ModuleName, message); + + using (EventLog eventLog = new EventLog("Application")) + { + eventLog.Source = logSource; + try + { + eventLog.WriteEntry(message, logEntryType, 0); + } + catch (System.InvalidOperationException) { } // Ignore permission errors on the Application event log + catch (System.Exception e) + { + // Cannot call Warn as that calls LogEvent and we get stuck in a loop + warnings.Add(String.Format("Unknown error when creating event log entry: {0}", e.Message)); + } + } +#else + // Windows Event Log is only available on Windows + return; +#endif + } + + public void Warn(string message) + { + warnings.Add(message); + LogEvent(String.Format("[WARNING] {0}", message), EventLogEntryType.Warning); + } + + public static object FromJson(string json) { return FromJson(json); } + public static T FromJson(string json) + { +#if CORECLR + return JsonConvert.DeserializeObject(json); +#else + JavaScriptSerializer jss = new JavaScriptSerializer(); + jss.MaxJsonLength = int.MaxValue; + jss.RecursionLimit = int.MaxValue; + return jss.Deserialize(json); +#endif + } + + public static string ToJson(object obj) + { + // Using PowerShell to serialize the JSON is preferable over the native .NET libraries as it handles + // PS Objects a lot better than the alternatives. In case we are debugging in Visual Studio we have a + // fallback to the other libraries as we won't be dealing with PowerShell objects there. + if (Runspace.DefaultRunspace != null) + { + PSObject rawOut = ScriptBlock.Create("ConvertTo-Json -InputObject $args[0] -Depth 99 -Compress").Invoke(obj)[0]; + return rawOut.BaseObject as string; + } + else + { +#if CORECLR + return JsonConvert.SerializeObject(obj); +#else + JavaScriptSerializer jss = new JavaScriptSerializer(); + jss.MaxJsonLength = int.MaxValue; + jss.RecursionLimit = int.MaxValue; + return jss.Serialize(obj); +#endif + } + } + + public static IDictionary GetParams(string[] args) + { + if (args.Length > 0) + { + string inputJson = File.ReadAllText(args[0]); + Dictionary rawParams = FromJson>(inputJson); + if (!rawParams.ContainsKey("ANSIBLE_MODULE_ARGS")) + throw new ArgumentException("Module was unable to get ANSIBLE_MODULE_ARGS value from the argument path json"); + return (IDictionary)rawParams["ANSIBLE_MODULE_ARGS"]; + } + else + { + // $complex_args is already a Hashtable, no need to waste time converting to a dictionary + PSObject rawArgs = ScriptBlock.Create("$complex_args").Invoke()[0]; + return rawArgs.BaseObject as Hashtable; + } + } + + public static bool ParseBool(object value) + { + if (value.GetType() == typeof(bool)) + return (bool)value; + + List booleans = new List(); + booleans.AddRange(BOOLEANS_TRUE); + booleans.AddRange(BOOLEANS_FALSE); + + string stringValue = ParseStr(value).ToLowerInvariant().Trim(); + if (BOOLEANS_TRUE.Contains(stringValue)) + return true; + else if (BOOLEANS_FALSE.Contains(stringValue)) + return false; + + string msg = String.Format("The value '{0}' is not a valid boolean. Valid booleans include: {1}", + stringValue, String.Join(", ", booleans)); + throw new ArgumentException(msg); + } + + public static Dictionary ParseDict(object value) + { + Type valueType = value.GetType(); + if (valueType == typeof(Dictionary)) + return (Dictionary)value; + else if (value is IDictionary) + return ((IDictionary)value).Cast().ToDictionary(kvp => (string)kvp.Key, kvp => kvp.Value); + else if (valueType == typeof(string)) + { + string stringValue = (string)value; + if (stringValue.StartsWith("{") && stringValue.EndsWith("}")) + return FromJson>((string)value); + else if (stringValue.IndexOfAny(new char[1] { '=' }) != -1) + { + List fields = new List(); + List fieldBuffer = new List(); + char? inQuote = null; + bool inEscape = false; + string field; + + foreach (char c in stringValue.ToCharArray()) + { + if (inEscape) + { + fieldBuffer.Add(c); + inEscape = false; + } + else if (c == '\\') + inEscape = true; + else if (inQuote == null && (c == '\'' || c == '"')) + inQuote = c; + else if (inQuote != null && c == inQuote) + inQuote = null; + else if (inQuote == null && (c == ',' || c == ' ')) + { + field = String.Join("", fieldBuffer); + if (field != "") + fields.Add(field); + fieldBuffer = new List(); + } + else + fieldBuffer.Add(c); + } + + field = String.Join("", fieldBuffer); + if (field != "") + fields.Add(field); + + return fields.Distinct().Select(i => i.Split(new[] { '=' }, 2)).ToDictionary(i => i[0], i => i.Length > 1 ? (object)i[1] : null); + } + else + throw new ArgumentException("string cannot be converted to a dict, must either be a JSON string or in the key=value form"); + } + + throw new ArgumentException(String.Format("{0} cannot be converted to a dict", valueType.FullName)); + } + + public static float ParseFloat(object value) + { + if (value.GetType() == typeof(float)) + return (float)value; + + string valueStr = ParseStr(value); + return float.Parse(valueStr); + } + + public static int ParseInt(object value) + { + Type valueType = value.GetType(); + if (valueType == typeof(int)) + return (int)value; + else + return Int32.Parse(ParseStr(value)); + } + + public static string ParseJson(object value) + { + // mostly used to ensure a dict is a json string as it may + // have been converted on the controller side + Type valueType = value.GetType(); + if (value is IDictionary) + return ToJson(value); + else if (valueType == typeof(string)) + return (string)value; + else + throw new ArgumentException(String.Format("{0} cannot be converted to json", valueType.FullName)); + } + + public static List ParseList(object value) + { + if (value == null) + return null; + + Type valueType = value.GetType(); + if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(List<>)) + return (List)value; + else if (valueType == typeof(ArrayList)) + return ((ArrayList)value).Cast().ToList(); + else if (valueType.IsArray) + return ((object[])value).ToList(); + else if (valueType == typeof(string)) + return ((string)value).Split(',').Select(s => s.Trim()).ToList(); + else if (valueType == typeof(int)) + return new List() { value }; + else + throw new ArgumentException(String.Format("{0} cannot be converted to a list", valueType.FullName)); + } + + public static string ParsePath(object value) + { + string stringValue = ParseStr(value); + + // do not validate, expand the env vars if it starts with \\?\ as + // it is a special path designed for the NT kernel to interpret + if (stringValue.StartsWith(@"\\?\")) + return stringValue; + + stringValue = Environment.ExpandEnvironmentVariables(stringValue); + if (stringValue.IndexOfAny(Path.GetInvalidPathChars()) != -1) + throw new ArgumentException("string value contains invalid path characters, cannot convert to path"); + + // will fire an exception if it contains any invalid chars + Path.GetFullPath(stringValue); + return stringValue; + } + + public static object ParseRaw(object value) { return value; } + + public static SecurityIdentifier ParseSid(object value) + { + string stringValue = ParseStr(value); + + try + { + return new SecurityIdentifier(stringValue); + } + catch (ArgumentException) { } // ignore failures string may not have been a SID + + NTAccount account = new NTAccount(stringValue); + return (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); + } + + public static string ParseStr(object value) { return value.ToString(); } + + private void ValidateArgumentSpec(IDictionary argumentSpec) + { + Dictionary changedValues = new Dictionary(); + foreach (DictionaryEntry entry in argumentSpec) + { + string key = (string)entry.Key; + + // validate the key is a valid argument spec key + if (!specDefaults.ContainsKey(key)) + { + string msg = String.Format("argument spec entry contains an invalid key '{0}', valid keys: {1}", + key, String.Join(", ", specDefaults.Keys)); + throw new ArgumentException(FormatOptionsContext(msg, " - ")); + } + + // ensure the value is casted to the type we expect + Type optionType = null; + if (entry.Value != null) + optionType = (Type)specDefaults[key][1]; + if (optionType != null) + { + Type actualType = entry.Value.GetType(); + bool invalid = false; + if (optionType.IsGenericType && optionType.GetGenericTypeDefinition() == typeof(List<>)) + { + // verify the actual type is not just a single value of the list type + Type entryType = optionType.GetGenericArguments()[0]; + object[] arrayElementTypes = new object[] + { + null, // ArrayList does not have an ElementType + entryType, + typeof(object), // Hope the object is actually entryType or it can at least be casted. + }; + + bool isArray = entry.Value is IList && arrayElementTypes.Contains(actualType.GetElementType()); + if (actualType == entryType || isArray) + { + object rawArray; + if (isArray) + rawArray = entry.Value; + else + rawArray = new object[1] { entry.Value }; + + MethodInfo castMethod = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(entryType); + MethodInfo toListMethod = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(entryType); + + var enumerable = castMethod.Invoke(null, new object[1] { rawArray }); + var newList = toListMethod.Invoke(null, new object[1] { enumerable }); + changedValues.Add(key, newList); + } + else if (actualType != optionType && !(actualType == typeof(List))) + invalid = true; + } + else + invalid = actualType != optionType; + + if (invalid) + { + string msg = String.Format("argument spec for '{0}' did not match expected type {1}: actual type {2}", + key, optionType.FullName, actualType.FullName); + throw new ArgumentException(FormatOptionsContext(msg, " - ")); + } + } + + // recursively validate the spec + if (key == "options" && entry.Value != null) + { + IDictionary optionsSpec = (IDictionary)entry.Value; + foreach (DictionaryEntry optionEntry in optionsSpec) + { + optionsContext.Add((string)optionEntry.Key); + IDictionary optionMeta = (IDictionary)optionEntry.Value; + ValidateArgumentSpec(optionMeta); + optionsContext.RemoveAt(optionsContext.Count - 1); + } + } + + // validate the type and elements key type values are known types + if (key == "type" || key == "elements" && entry.Value != null) + { + Type valueType = entry.Value.GetType(); + if (valueType == typeof(string)) + { + string typeValue = (string)entry.Value; + if (!optionTypes.ContainsKey(typeValue)) + { + string msg = String.Format("{0} '{1}' is unsupported", key, typeValue); + msg = String.Format("{0}. Valid types are: {1}", FormatOptionsContext(msg, " - "), String.Join(", ", optionTypes.Keys)); + throw new ArgumentException(msg); + } + } + else if (!(entry.Value is Delegate)) + { + string msg = String.Format("{0} must either be a string or delegate, was: {1}", key, valueType.FullName); + throw new ArgumentException(FormatOptionsContext(msg, " - ")); + } + } + } + + // Outside of the spec iterator, change the values that were casted above + foreach (KeyValuePair changedValue in changedValues) + argumentSpec[changedValue.Key] = changedValue.Value; + } + + private void MergeFragmentSpec(IDictionary argumentSpec, IDictionary fragment) + { + foreach (DictionaryEntry fragmentEntry in fragment) + { + string fragmentKey = fragmentEntry.Key.ToString(); + + if (argumentSpec.Contains(fragmentKey)) + { + // We only want to add new list entries and merge dictionary new keys and values. Leave the other + // values as is in the argument spec as that takes priority over the fragment. + if (fragmentEntry.Value is IDictionary) + { + MergeFragmentSpec((IDictionary)argumentSpec[fragmentKey], (IDictionary)fragmentEntry.Value); + } + else if (fragmentEntry.Value is IList) + { + IList specValue = (IList)argumentSpec[fragmentKey]; + foreach (object fragmentValue in (IList)fragmentEntry.Value) + specValue.Add(fragmentValue); + } + } + else + argumentSpec[fragmentKey] = fragmentEntry.Value; + } + } + + private void SetArgumentSpecDefaults(IDictionary argumentSpec) + { + foreach (KeyValuePair> metadataEntry in specDefaults) + { + List defaults = metadataEntry.Value; + object defaultValue = defaults[0]; + if (defaultValue != null && defaultValue.GetType() == typeof(Type).GetType()) + defaultValue = Activator.CreateInstance((Type)defaultValue); + + if (!argumentSpec.Contains(metadataEntry.Key)) + argumentSpec[metadataEntry.Key] = defaultValue; + } + + // Recursively set the defaults for any inner options. + foreach (DictionaryEntry entry in argumentSpec) + { + if (entry.Value == null || entry.Key.ToString() != "options") + continue; + + IDictionary optionsSpec = (IDictionary)entry.Value; + foreach (DictionaryEntry optionEntry in optionsSpec) + { + optionsContext.Add((string)optionEntry.Key); + IDictionary optionMeta = (IDictionary)optionEntry.Value; + SetArgumentSpecDefaults(optionMeta); + optionsContext.RemoveAt(optionsContext.Count - 1); + } + } + } + + private Dictionary GetAliases(IDictionary argumentSpec, IDictionary parameters) + { + Dictionary aliasResults = new Dictionary(); + + foreach (DictionaryEntry entry in (IDictionary)argumentSpec["options"]) + { + string k = (string)entry.Key; + Hashtable v = (Hashtable)entry.Value; + + List aliases = (List)v["aliases"]; + object defaultValue = v["default"]; + bool required = (bool)v["required"]; + + if (defaultValue != null && required) + throw new ArgumentException(String.Format("required and default are mutually exclusive for {0}", k)); + + foreach (string alias in aliases) + { + aliasResults.Add(alias, k); + if (parameters.Contains(alias)) + parameters[k] = parameters[alias]; + } + + List deprecatedAliases = (List)v["deprecated_aliases"]; + foreach (Hashtable depInfo in deprecatedAliases) + { + foreach (string keyName in new List { "name" }) + { + if (!depInfo.ContainsKey(keyName)) + { + string msg = String.Format("{0} is required in a deprecated_aliases entry", keyName); + throw new ArgumentException(FormatOptionsContext(msg, " - ")); + } + } + if (!depInfo.ContainsKey("version") && !depInfo.ContainsKey("date")) + { + string msg = "One of version or date is required in a deprecated_aliases entry"; + throw new ArgumentException(FormatOptionsContext(msg, " - ")); + } + if (depInfo.ContainsKey("version") && depInfo.ContainsKey("date")) + { + string msg = "Only one of version or date is allowed in a deprecated_aliases entry"; + throw new ArgumentException(FormatOptionsContext(msg, " - ")); + } + if (depInfo.ContainsKey("date") && depInfo["date"].GetType() != typeof(DateTime)) + { + string msg = "A deprecated_aliases date must be a DateTime object"; + throw new ArgumentException(FormatOptionsContext(msg, " - ")); + } + string collectionName = null; + if (depInfo.ContainsKey("collection_name")) + { + collectionName = (string)depInfo["collection_name"]; + } + string aliasName = (string)depInfo["name"]; + + if (parameters.Contains(aliasName)) + { + string msg = String.Format("Alias '{0}' is deprecated. See the module docs for more information", aliasName); + if (depInfo.ContainsKey("version")) + { + string depVersion = (string)depInfo["version"]; + Deprecate(FormatOptionsContext(msg, " - "), depVersion, collectionName); + } + if (depInfo.ContainsKey("date")) + { + DateTime depDate = (DateTime)depInfo["date"]; + Deprecate(FormatOptionsContext(msg, " - "), depDate, collectionName); + } + } + } + } + + return aliasResults; + } + + private void SetNoLogValues(IDictionary argumentSpec, IDictionary parameters) + { + foreach (DictionaryEntry entry in (IDictionary)argumentSpec["options"]) + { + string k = (string)entry.Key; + Hashtable v = (Hashtable)entry.Value; + + if ((bool)v["no_log"]) + { + object noLogObject = parameters.Contains(k) ? parameters[k] : null; + string noLogString = noLogObject == null ? "" : noLogObject.ToString(); + if (!String.IsNullOrEmpty(noLogString)) + noLogValues.Add(noLogString); + } + string collectionName = null; + if (v.ContainsKey("removed_from_collection")) + { + collectionName = (string)v["removed_from_collection"]; + } + + object removedInVersion = v["removed_in_version"]; + if (removedInVersion != null && parameters.Contains(k)) + Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k), + removedInVersion.ToString(), collectionName); + + object removedAtDate = v["removed_at_date"]; + if (removedAtDate != null && parameters.Contains(k)) + Deprecate(String.Format("Param '{0}' is deprecated. See the module docs for more information", k), + (DateTime)removedAtDate, collectionName); + } + } + + private void CheckArguments(IDictionary spec, IDictionary param, List legalInputs) + { + // initially parse the params and check for unsupported ones and set internal vars + CheckUnsupportedArguments(param, legalInputs); + + // Only run this check if we are at the root argument (optionsContext.Count == 0) + if (CheckMode && !(bool)spec["supports_check_mode"] && optionsContext.Count == 0) + { + Result["skipped"] = true; + Result["msg"] = String.Format("remote module ({0}) does not support check mode", ModuleName); + ExitJson(); + } + IDictionary optionSpec = (IDictionary)spec["options"]; + + CheckMutuallyExclusive(param, (IList)spec["mutually_exclusive"]); + CheckRequiredArguments(optionSpec, param); + + // set the parameter types based on the type spec value + foreach (DictionaryEntry entry in optionSpec) + { + string k = (string)entry.Key; + Hashtable v = (Hashtable)entry.Value; + + object value = param.Contains(k) ? param[k] : null; + if (value != null) + { + // convert the current value to the wanted type + Delegate typeConverter; + string type; + if (v["type"].GetType() == typeof(string)) + { + type = (string)v["type"]; + typeConverter = optionTypes[type]; + } + else + { + type = "delegate"; + typeConverter = (Delegate)v["type"]; + } + + try + { + value = typeConverter.DynamicInvoke(value); + param[k] = value; + } + catch (Exception e) + { + string msg = String.Format("argument for {0} is of type {1} and we were unable to convert to {2}: {3}", + k, value.GetType(), type, e.InnerException.Message); + FailJson(FormatOptionsContext(msg)); + } + + // ensure it matches the choices if there are choices set + List choices = ((List)v["choices"]).Select(x => x.ToString()).Cast().ToList(); + if (choices.Count > 0) + { + List values; + string choiceMsg; + if (type == "list") + { + values = ((List)value).Select(x => x.ToString()).Cast().ToList(); + choiceMsg = "one or more of"; + } + else + { + values = new List() { value.ToString() }; + choiceMsg = "one of"; + } + + List diffList = values.Except(choices, StringComparer.OrdinalIgnoreCase).ToList(); + List caseDiffList = values.Except(choices).ToList(); + if (diffList.Count > 0) + { + string msg = String.Format("value of {0} must be {1}: {2}. Got no match for: {3}", + k, choiceMsg, String.Join(", ", choices), String.Join(", ", diffList)); + FailJson(FormatOptionsContext(msg)); + } + /* + For now we will just silently accept case insensitive choices, uncomment this if we want to add it back in + else if (caseDiffList.Count > 0) + { + // For backwards compatibility with Legacy.psm1 we need to be matching choices that are not case sensitive. + // We will warn the user it was case insensitive and tell them this will become case sensitive in the future. + string msg = String.Format( + "value of {0} was a case insensitive match of {1}: {2}. Checking of choices will be case sensitive in a future Ansible release. Case insensitive matches were: {3}", + k, choiceMsg, String.Join(", ", choices), String.Join(", ", caseDiffList.Select(x => RemoveNoLogValues(x, noLogValues))) + ); + Warn(FormatOptionsContext(msg)); + }*/ + } + } + } + + CheckRequiredTogether(param, (IList)spec["required_together"]); + CheckRequiredOneOf(param, (IList)spec["required_one_of"]); + CheckRequiredIf(param, (IList)spec["required_if"]); + CheckRequiredBy(param, (IDictionary)spec["required_by"]); + + // finally ensure all missing parameters are set to null and handle sub options + foreach (DictionaryEntry entry in optionSpec) + { + string k = (string)entry.Key; + IDictionary v = (IDictionary)entry.Value; + + if (!param.Contains(k)) + param[k] = null; + + CheckSubOption(param, k, v); + } + } + + private void CheckUnsupportedArguments(IDictionary param, List legalInputs) + { + HashSet unsupportedParameters = new HashSet(); + HashSet caseUnsupportedParameters = new HashSet(); + List removedParameters = new List(); + + foreach (DictionaryEntry entry in param) + { + string paramKey = (string)entry.Key; + if (!legalInputs.Contains(paramKey, StringComparer.OrdinalIgnoreCase)) + unsupportedParameters.Add(paramKey); + else if (!legalInputs.Contains(paramKey)) + // For backwards compatibility we do not care about the case but we need to warn the users as this will + // change in a future Ansible release. + caseUnsupportedParameters.Add(paramKey); + else if (paramKey.StartsWith("_ansible_")) + { + removedParameters.Add(paramKey); + string key = paramKey.Replace("_ansible_", ""); + // skip setting NoLog if NoLog is already set to true (set by the module) + // or there's no mapping for this key + if ((key == "no_log" && NoLog == true) || (passVars[key] == null)) + continue; + + object value = entry.Value; + if (passBools.Contains(key)) + value = ParseBool(value); + else if (passInts.Contains(key)) + value = ParseInt(value); + + string propertyName = passVars[key]; + PropertyInfo property = typeof(AnsibleModule).GetProperty(propertyName); + FieldInfo field = typeof(AnsibleModule).GetField(propertyName, BindingFlags.NonPublic | BindingFlags.Instance); + if (property != null) + property.SetValue(this, value, null); + else if (field != null) + field.SetValue(this, value); + else + FailJson(String.Format("implementation error: unknown AnsibleModule property {0}", propertyName)); + } + } + foreach (string parameter in removedParameters) + param.Remove(parameter); + + if (unsupportedParameters.Count > 0) + { + legalInputs.RemoveAll(x => passVars.Keys.Contains(x.Replace("_ansible_", ""))); + string msg = String.Format("Unsupported parameters for ({0}) module: {1}", ModuleName, String.Join(", ", unsupportedParameters)); + msg = String.Format("{0}. Supported parameters include: {1}", FormatOptionsContext(msg), String.Join(", ", legalInputs)); + FailJson(msg); + } + + /* + // Uncomment when we want to start warning users around options that are not a case sensitive match to the spec + if (caseUnsupportedParameters.Count > 0) + { + legalInputs.RemoveAll(x => passVars.Keys.Contains(x.Replace("_ansible_", ""))); + string msg = String.Format("Parameters for ({0}) was a case insensitive match: {1}", ModuleName, String.Join(", ", caseUnsupportedParameters)); + msg = String.Format("{0}. Module options will become case sensitive in a future Ansible release. Supported parameters include: {1}", + FormatOptionsContext(msg), String.Join(", ", legalInputs)); + Warn(msg); + }*/ + + // Make sure we convert all the incorrect case params to the ones set by the module spec + foreach (string key in caseUnsupportedParameters) + { + string correctKey = legalInputs[legalInputs.FindIndex(s => s.Equals(key, StringComparison.OrdinalIgnoreCase))]; + object value = param[key]; + param.Remove(key); + param.Add(correctKey, value); + } + } + + private void CheckMutuallyExclusive(IDictionary param, IList mutuallyExclusive) + { + if (mutuallyExclusive == null) + return; + + foreach (object check in mutuallyExclusive) + { + List mutualCheck = ((IList)check).Cast().ToList(); + int count = 0; + foreach (string entry in mutualCheck) + if (param.Contains(entry)) + count++; + + if (count > 1) + { + string msg = String.Format("parameters are mutually exclusive: {0}", String.Join(", ", mutualCheck)); + FailJson(FormatOptionsContext(msg)); + } + } + } + + private void CheckRequiredArguments(IDictionary spec, IDictionary param) + { + List missing = new List(); + foreach (DictionaryEntry entry in spec) + { + string k = (string)entry.Key; + Hashtable v = (Hashtable)entry.Value; + + // set defaults for values not already set + object defaultValue = v["default"]; + if (defaultValue != null && !param.Contains(k)) + param[k] = defaultValue; + + // check required arguments + bool required = (bool)v["required"]; + if (required && !param.Contains(k)) + missing.Add(k); + } + if (missing.Count > 0) + { + string msg = String.Format("missing required arguments: {0}", String.Join(", ", missing)); + FailJson(FormatOptionsContext(msg)); + } + } + + private void CheckRequiredTogether(IDictionary param, IList requiredTogether) + { + if (requiredTogether == null) + return; + + foreach (object check in requiredTogether) + { + List requiredCheck = ((IList)check).Cast().ToList(); + List found = new List(); + foreach (string field in requiredCheck) + if (param.Contains(field)) + found.Add(true); + else + found.Add(false); + + if (found.Contains(true) && found.Contains(false)) + { + string msg = String.Format("parameters are required together: {0}", String.Join(", ", requiredCheck)); + FailJson(FormatOptionsContext(msg)); + } + } + } + + private void CheckRequiredOneOf(IDictionary param, IList requiredOneOf) + { + if (requiredOneOf == null) + return; + + foreach (object check in requiredOneOf) + { + List requiredCheck = ((IList)check).Cast().ToList(); + int count = 0; + foreach (string field in requiredCheck) + if (param.Contains(field)) + count++; + + if (count == 0) + { + string msg = String.Format("one of the following is required: {0}", String.Join(", ", requiredCheck)); + FailJson(FormatOptionsContext(msg)); + } + } + } + + private void CheckRequiredIf(IDictionary param, IList requiredIf) + { + if (requiredIf == null) + return; + + foreach (object check in requiredIf) + { + IList requiredCheck = (IList)check; + List missing = new List(); + List missingFields = new List(); + int maxMissingCount = 1; + bool oneRequired = false; + + if (requiredCheck.Count < 3 && requiredCheck.Count < 4) + FailJson(String.Format("internal error: invalid required_if value count of {0}, expecting 3 or 4 entries", requiredCheck.Count)); + else if (requiredCheck.Count == 4) + oneRequired = (bool)requiredCheck[3]; + + string key = (string)requiredCheck[0]; + object val = requiredCheck[1]; + IList requirements = (IList)requiredCheck[2]; + + if (ParseStr(param[key]) != ParseStr(val)) + continue; + + string term = "all"; + if (oneRequired) + { + maxMissingCount = requirements.Count; + term = "any"; + } + + foreach (string required in requirements.Cast()) + if (!param.Contains(required)) + missing.Add(required); + + if (missing.Count >= maxMissingCount) + { + string msg = String.Format("{0} is {1} but {2} of the following are missing: {3}", + key, val.ToString(), term, String.Join(", ", missing)); + FailJson(FormatOptionsContext(msg)); + } + } + } + + private void CheckRequiredBy(IDictionary param, IDictionary requiredBy) + { + foreach (DictionaryEntry entry in requiredBy) + { + string key = (string)entry.Key; + if (!param.Contains(key)) + continue; + + List missing = new List(); + List requires = ParseList(entry.Value).Cast().ToList(); + foreach (string required in requires) + if (!param.Contains(required)) + missing.Add(required); + + if (missing.Count > 0) + { + string msg = String.Format("missing parameter(s) required by '{0}': {1}", key, String.Join(", ", missing)); + FailJson(FormatOptionsContext(msg)); + } + } + } + + private void CheckSubOption(IDictionary param, string key, IDictionary spec) + { + object value = param[key]; + + string type; + if (spec["type"].GetType() == typeof(string)) + type = (string)spec["type"]; + else + type = "delegate"; + + string elements = null; + Delegate typeConverter = null; + if (spec["elements"] != null && spec["elements"].GetType() == typeof(string)) + { + elements = (string)spec["elements"]; + typeConverter = optionTypes[elements]; + } + else if (spec["elements"] != null) + { + elements = "delegate"; + typeConverter = (Delegate)spec["elements"]; + } + + if (!(type == "dict" || (type == "list" && elements != null))) + // either not a dict, or list with the elements set, so continue + return; + else if (type == "list") + { + // cast each list element to the type specified + if (value == null) + return; + + List newValue = new List(); + foreach (object element in (List)value) + { + if (elements == "dict") + newValue.Add(ParseSubSpec(spec, element, key)); + else + { + try + { + object newElement = typeConverter.DynamicInvoke(element); + newValue.Add(newElement); + } + catch (Exception e) + { + string msg = String.Format("argument for list entry {0} is of type {1} and we were unable to convert to {2}: {3}", + key, element.GetType(), elements, e.Message); + FailJson(FormatOptionsContext(msg)); + } + } + } + + param[key] = newValue; + } + else + param[key] = ParseSubSpec(spec, value, key); + } + + private object ParseSubSpec(IDictionary spec, object value, string context) + { + bool applyDefaults = (bool)spec["apply_defaults"]; + + // set entry to an empty dict if apply_defaults is set + IDictionary optionsSpec = (IDictionary)spec["options"]; + if (applyDefaults && optionsSpec.Keys.Count > 0 && value == null) + value = new Dictionary(); + else if (optionsSpec.Keys.Count == 0 || value == null) + return value; + + optionsContext.Add(context); + Dictionary newValue = (Dictionary)ParseDict(value); + Dictionary aliases = GetAliases(spec, newValue); + SetNoLogValues(spec, newValue); + + List subLegalInputs = optionsSpec.Keys.Cast().ToList(); + subLegalInputs.AddRange(aliases.Keys.Cast().ToList()); + + CheckArguments(spec, newValue, subLegalInputs); + optionsContext.RemoveAt(optionsContext.Count - 1); + return newValue; + } + + private string GetFormattedResults(Dictionary result) + { + if (!result.ContainsKey("invocation")) + result["invocation"] = new Dictionary() { { "module_args", RemoveNoLogValues(Params, noLogValues) } }; + + if (warnings.Count > 0) + result["warnings"] = warnings; + + if (deprecations.Count > 0) + result["deprecations"] = deprecations; + + if (Diff.Count > 0 && DiffMode) + result["diff"] = Diff; + + return ToJson(result); + } + + private string FormatLogData(object data, int indentLevel) + { + if (data == null) + return "$null"; + + string msg = ""; + if (data is IList) + { + string newMsg = ""; + foreach (object value in (IList)data) + { + string entryValue = FormatLogData(value, indentLevel + 2); + newMsg += String.Format("\r\n{0}- {1}", new String(' ', indentLevel), entryValue); + } + msg += newMsg; + } + else if (data is IDictionary) + { + bool start = true; + foreach (DictionaryEntry entry in (IDictionary)data) + { + string newMsg = FormatLogData(entry.Value, indentLevel + 2); + if (!start) + msg += String.Format("\r\n{0}", new String(' ', indentLevel)); + msg += String.Format("{0}: {1}", (string)entry.Key, newMsg); + start = false; + } + } + else + msg = (string)RemoveNoLogValues(ParseStr(data), noLogValues); + + return msg; + } + + private object RemoveNoLogValues(object value, HashSet noLogStrings) + { + Queue> deferredRemovals = new Queue>(); + object newValue = RemoveValueConditions(value, noLogStrings, deferredRemovals); + + while (deferredRemovals.Count > 0) + { + Tuple data = deferredRemovals.Dequeue(); + object oldData = data.Item1; + object newData = data.Item2; + + if (oldData is IDictionary) + { + foreach (DictionaryEntry entry in (IDictionary)oldData) + { + object newElement = RemoveValueConditions(entry.Value, noLogStrings, deferredRemovals); + ((IDictionary)newData).Add((string)entry.Key, newElement); + } + } + else + { + foreach (object element in (IList)oldData) + { + object newElement = RemoveValueConditions(element, noLogStrings, deferredRemovals); + ((IList)newData).Add(newElement); + } + } + } + + return newValue; + } + + private object RemoveValueConditions(object value, HashSet noLogStrings, Queue> deferredRemovals) + { + if (value == null) + return value; + + Type valueType = value.GetType(); + HashSet numericTypes = new HashSet + { + typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(decimal), typeof(double), typeof(float) + }; + + if (numericTypes.Contains(valueType) || valueType == typeof(bool)) + { + string valueString = ParseStr(value); + if (noLogStrings.Contains(valueString)) + return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"; + foreach (string omitMe in noLogStrings) + if (valueString.Contains(omitMe)) + return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"; + } + else if (valueType == typeof(DateTime)) + value = ((DateTime)value).ToString("o"); + else if (value is IList) + { + List newValue = new List(); + deferredRemovals.Enqueue(new Tuple((IList)value, newValue)); + value = newValue; + } + else if (value is IDictionary) + { + Hashtable newValue = new Hashtable(); + deferredRemovals.Enqueue(new Tuple((IDictionary)value, newValue)); + value = newValue; + } + else + { + string stringValue = value.ToString(); + if (noLogStrings.Contains(stringValue)) + return "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"; + foreach (string omitMe in noLogStrings) + if (stringValue.Contains(omitMe)) + return (stringValue).Replace(omitMe, "********"); + value = stringValue; + } + return value; + } + + private void CleanupFiles(object s, EventArgs ev) + { + foreach (string path in cleanupFiles) + { + if (File.Exists(path)) + File.Delete(path); + else if (Directory.Exists(path)) + Directory.Delete(path, true); + } + cleanupFiles = new List(); + } + + private string FormatOptionsContext(string msg, string prefix = " ") + { + if (optionsContext.Count > 0) + msg += String.Format("{0}found in {1}", prefix, String.Join(" -> ", optionsContext)); + return msg; + } + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + private static void ExitModule(int rc) + { + // When running in a Runspace Environment.Exit will kill the entire + // process which is not what we want, detect if we are in a + // Runspace and call a ScriptBlock with exit instead. + if (Runspace.DefaultRunspace != null) + ScriptBlock.Create("Set-Variable -Name LASTEXITCODE -Value $args[0] -Scope Global; exit $args[0]").Invoke(rc); + else + { + // Used for local debugging in Visual Studio + if (System.Diagnostics.Debugger.IsAttached) + { + Console.WriteLine("Press enter to continue..."); + Console.ReadLine(); + } + Environment.Exit(rc); + } + } + + private static void WriteLineModule(string line) + { + Console.WriteLine(line); + } + } +} diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.ModuleUtils.AddType.psm1 b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.ModuleUtils.AddType.psm1 new file mode 100644 index 000000000..673070263 --- /dev/null +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/module_utils/_Ansible.ModuleUtils.AddType.psm1 @@ -0,0 +1,397 @@ +# Copyright (c) 2018 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +Function Add-CSharpType { + <# + .SYNOPSIS + Compiles one or more C# scripts similar to Add-Type. This exposes + more configuration options that are useable within Ansible and it + also allows multiple C# sources to be compiled together. + + .PARAMETER References + [String[]] A collection of C# scripts to compile together. + + .PARAMETER IgnoreWarnings + [Switch] Whether to compile code that contains compiler warnings, by + default warnings will cause a compiler error. + + .PARAMETER PassThru + [Switch] Whether to return the loaded Assembly + + .PARAMETER AnsibleModule + [Ansible.Basic.AnsibleModule] used to derive the TempPath and Debug values. + TempPath is set to the Tmpdir property of the class + IncludeDebugInfo is set when the Ansible verbosity is >= 3 + + .PARAMETER TempPath + [String] The temporary directory in which the dynamic assembly is + compiled to. This file is deleted once compilation is complete. + Cannot be used when AnsibleModule is set. This is a no-op when + running on PSCore. + + .PARAMETER IncludeDebugInfo + [Switch] Whether to include debug information in the compiled + assembly. Cannot be used when AnsibleModule is set. This is a no-op + when running on PSCore. + + .PARAMETER CompileSymbols + [String[]] A list of symbols to be defined during compile time. These are + added to the existing symbols, 'CORECLR', 'WINDOWS', 'UNIX' that are set + conditionalls in this cmdlet. + + .NOTES + The following features were added to control the compiling options from the + code itself. + + * Predefined compiler SYMBOLS + + * CORECLR - Added when running on PowerShell Core. + * WINDOWS - Added when running on Windows. + * UNIX - Added when running on non-Windows. + * X86 - Added when running on a 32-bit process (Ansible 2.10+) + * AMD64 - Added when running on a 64-bit process (Ansible 2.10+) + + * Ignore compiler warnings inline with the following comment inline + + //NoWarn -Name [-CLR Core|Framework] + + * Specify custom assembly references inline + + //AssemblyReference -Name Dll.Location.dll [-CLR Core|Framework] + + # Added in Ansible 2.10 + //AssemblyReference -Type System.Type.Name [-CLR Core|Framework] + + * Create automatic type accelerators to simplify long namespace names (Ansible 2.9+) + + //TypeAccelerator -Name -TypeName + #> + param( + [Parameter(Mandatory = $true)][AllowEmptyCollection()][String[]]$References, + [Switch]$IgnoreWarnings, + [Switch]$PassThru, + [Parameter(Mandatory = $true, ParameterSetName = "Module")][Object]$AnsibleModule, + [Parameter(ParameterSetName = "Manual")][String]$TempPath = $env:TMP, + [Parameter(ParameterSetName = "Manual")][Switch]$IncludeDebugInfo, + [String[]]$CompileSymbols = @() + ) + if ($null -eq $References -or $References.Length -eq 0) { + return + } + + # define special symbols CORECLR, WINDOWS, UNIX if required + # the Is* variables are defined on PSCore, if absent we assume an + # older version of PowerShell under .NET Framework and Windows + $defined_symbols = [System.Collections.ArrayList]$CompileSymbols + + if ([System.IntPtr]::Size -eq 4) { + $defined_symbols.Add('X86') > $null + } + else { + $defined_symbols.Add('AMD64') > $null + } + + $is_coreclr = Get-Variable -Name IsCoreCLR -ErrorAction SilentlyContinue + if ($null -ne $is_coreclr) { + if ($is_coreclr.Value) { + $defined_symbols.Add("CORECLR") > $null + } + } + $is_windows = Get-Variable -Name IsWindows -ErrorAction SilentlyContinue + if ($null -ne $is_windows) { + if ($is_windows.Value) { + $defined_symbols.Add("WINDOWS") > $null + } + else { + $defined_symbols.Add("UNIX") > $null + } + } + else { + $defined_symbols.Add("WINDOWS") > $null + } + + # Store any TypeAccelerators shortcuts the util wants us to set + $type_accelerators = [System.Collections.Generic.List`1[Hashtable]]@() + + # pattern used to find referenced assemblies in the code + $assembly_pattern = [Regex]"//\s*AssemblyReference\s+-(?(Name)|(Type))\s+(?[\w.]*)(\s+-CLR\s+(?Core|Framework))?" + $no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?[\w\d]*)(\s+-CLR\s+(?Core|Framework))?" + $type_pattern = [Regex]"//\s*TypeAccelerator\s+-Name\s+(?[\w.]*)\s+-TypeName\s+(?[\w.]*)" + + # PSCore vs PSDesktop use different methods to compile the code, + # PSCore uses Roslyn and can compile the code purely in memory + # without touching the disk while PSDesktop uses CodeDom and csc.exe + # to compile the code. We branch out here and run each + # distribution's method to add our C# code. + if ($is_coreclr) { + # compile the code using Roslyn on PSCore + + # Include the default assemblies using the logic in Add-Type + # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs + $assemblies = [System.Collections.Generic.HashSet`1[Microsoft.CodeAnalysis.MetadataReference]]@( + [Microsoft.CodeAnalysis.CompilationReference]::CreateFromFile(([System.Reflection.Assembly]::GetAssembly([PSObject])).Location) + ) + $netcore_app_ref_folder = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName([PSObject].Assembly.Location), "ref") + $lib_assembly_location = [System.IO.Path]::GetDirectoryName([object].Assembly.Location) + foreach ($file in [System.IO.Directory]::EnumerateFiles($netcore_app_ref_folder, "*.dll", [System.IO.SearchOption]::TopDirectoryOnly)) { + $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($file)) > $null + } + + # loop through the references, parse as a SyntaxTree and get + # referenced assemblies + $ignore_warnings = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[String], [Microsoft.CodeAnalysis.ReportDiagnostic]]' + $parse_options = ([Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default).WithPreprocessorSymbols($defined_symbols) + $syntax_trees = [System.Collections.Generic.List`1[Microsoft.CodeAnalysis.SyntaxTree]]@() + foreach ($reference in $References) { + # scan through code and add any assemblies that match + # //AssemblyReference -Name ... [-CLR Core] + # //NoWarn -Name ... [-CLR Core] + # //TypeAccelerator -Name ... -TypeName ... + $assembly_matches = $assembly_pattern.Matches($reference) + foreach ($match in $assembly_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Core") { + continue + } + + $parameter_type = $match.Groups["Parameter"].Value + $assembly_path = $match.Groups["Name"].Value + if ($parameter_type -eq "Type") { + $assembly_path = ([Type]$assembly_path).Assembly.Location + } + else { + if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) { + $assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path + } + } + $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null + } + $warn_matches = $no_warn_pattern.Matches($reference) + foreach ($match in $warn_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Core") { + continue + } + $ignore_warnings.Add($match.Groups["Name"], [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress) + } + $syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null + + $type_matches = $type_pattern.Matches($reference) + foreach ($match in $type_matches) { + $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value }) + } + } + + # Release seems to contain the correct line numbers compared to + # debug,may need to keep a closer eye on this in the future + $compiler_options = (New-Object -TypeName Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions -ArgumentList @( + [Microsoft.CodeAnalysis.OutputKind]::DynamicallyLinkedLibrary + )).WithOptimizationLevel([Microsoft.CodeAnalysis.OptimizationLevel]::Release) + + # set warnings to error out if IgnoreWarnings is not set + if (-not $IgnoreWarnings.IsPresent) { + $compiler_options = $compiler_options.WithGeneralDiagnosticOption([Microsoft.CodeAnalysis.ReportDiagnostic]::Error) + $compiler_options = $compiler_options.WithSpecificDiagnosticOptions($ignore_warnings) + } + + # create compilation object + $compilation = [Microsoft.CodeAnalysis.CSharp.CSharpCompilation]::Create( + [System.Guid]::NewGuid().ToString(), + $syntax_trees, + $assemblies, + $compiler_options + ) + + # Load the compiled code and pdb info, we do this so we can + # include line number in a stracktrace + $code_ms = New-Object -TypeName System.IO.MemoryStream + $pdb_ms = New-Object -TypeName System.IO.MemoryStream + try { + $emit_result = $compilation.Emit($code_ms, $pdb_ms) + if (-not $emit_result.Success) { + $errors = [System.Collections.ArrayList]@() + + foreach ($e in $emit_result.Diagnostics) { + # builds the error msg, based on logic in Add-Type + # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs#L1239 + if ($null -eq $e.Location.SourceTree) { + $errors.Add($e.ToString()) > $null + continue + } + + $cancel_token = New-Object -TypeName System.Threading.CancellationToken -ArgumentList $false + $text_lines = $e.Location.SourceTree.GetText($cancel_token).Lines + $line_span = $e.Location.GetLineSpan() + + $diagnostic_message = $e.ToString() + $error_line_string = $text_lines[$line_span.StartLinePosition.Line].ToString() + $error_position = $line_span.StartLinePosition.Character + + $sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($diagnostic_message.Length + $error_line_string.Length * 2 + 4) + $sb.AppendLine($diagnostic_message) + $sb.AppendLine($error_line_string) + + for ($i = 0; $i -lt $error_line_string.Length; $i++) { + if ([System.Char]::IsWhiteSpace($error_line_string[$i])) { + continue + } + $sb.Append($error_line_string, 0, $i) + $sb.Append(' ', [Math]::Max(0, $error_position - $i)) + $sb.Append("^") + break + } + + $errors.Add($sb.ToString()) > $null + } + + throw [InvalidOperationException]"Failed to compile C# code:`r`n$($errors -join "`r`n")" + } + + $code_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $pdb_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $compiled_assembly = [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($code_ms, $pdb_ms) + } + finally { + $code_ms.Close() + $pdb_ms.Close() + } + } + else { + # compile the code using CodeDom on PSDesktop + + # configure compile options based on input + if ($PSCmdlet.ParameterSetName -eq "Module") { + $temp_path = $AnsibleModule.Tmpdir + $include_debug = $AnsibleModule.Verbosity -ge 3 + } + else { + $temp_path = $TempPath + $include_debug = $IncludeDebugInfo.IsPresent + } + $compiler_options = [System.Collections.ArrayList]@("/optimize") + if ($defined_symbols.Count -gt 0) { + $compiler_options.Add("/define:" + ([String]::Join(";", $defined_symbols.ToArray()))) > $null + } + + $compile_parameters = New-Object -TypeName System.CodeDom.Compiler.CompilerParameters + $compile_parameters.GenerateExecutable = $false + $compile_parameters.GenerateInMemory = $true + $compile_parameters.TreatWarningsAsErrors = (-not $IgnoreWarnings.IsPresent) + $compile_parameters.IncludeDebugInformation = $include_debug + $compile_parameters.TempFiles = (New-Object -TypeName System.CodeDom.Compiler.TempFileCollection -ArgumentList $temp_path, $false) + + # Add-Type automatically references System.dll, System.Core.dll, + # and System.Management.Automation.dll which we replicate here + $assemblies = [System.Collections.Generic.HashSet`1[String]]@( + "System.dll", + "System.Core.dll", + ([System.Reflection.Assembly]::GetAssembly([PSObject])).Location + ) + + # create a code snippet for each reference and check if we need + # to reference any extra assemblies + $ignore_warnings = [System.Collections.ArrayList]@() + $compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@() + foreach ($reference in $References) { + # scan through code and add any assemblies that match + # //AssemblyReference -Name ... [-CLR Framework] + # //NoWarn -Name ... [-CLR Framework] + # //TypeAccelerator -Name ... -TypeName ... + $assembly_matches = $assembly_pattern.Matches($reference) + foreach ($match in $assembly_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Framework") { + continue + } + + $parameter_type = $match.Groups["Parameter"].Value + $assembly_path = $match.Groups["Name"].Value + if ($parameter_type -eq "Type") { + $assembly_path = ([Type]$assembly_path).Assembly.Location + } + $assemblies.Add($assembly_path) > $null + } + $warn_matches = $no_warn_pattern.Matches($reference) + foreach ($match in $warn_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Framework") { + continue + } + $warning_id = $match.Groups["Name"].Value + # /nowarn should only contain the numeric part + if ($warning_id.StartsWith("CS")) { + $warning_id = $warning_id.Substring(2) + } + $ignore_warnings.Add($warning_id) > $null + } + $compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null + + $type_matches = $type_pattern.Matches($reference) + foreach ($match in $type_matches) { + $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value }) + } + } + if ($ignore_warnings.Count -gt 0) { + $compiler_options.Add("/nowarn:" + ([String]::Join(",", $ignore_warnings.ToArray()))) > $null + } + $compile_parameters.ReferencedAssemblies.AddRange($assemblies) + $compile_parameters.CompilerOptions = [String]::Join(" ", $compiler_options.ToArray()) + + # compile the code together and check for errors + $provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider + + # This calls csc.exe which can take compiler options from environment variables. Currently these env vars + # are known to have problems so they are unset: + # LIB - additional library paths will fail the compilation if they are invalid + $originalEnv = @{} + try { + 'LIB' | ForEach-Object -Process { + $value = Get-Item -LiteralPath "Env:\$_" -ErrorAction SilentlyContinue + if ($value) { + $originalEnv[$_] = $value + Remove-Item -LiteralPath "Env:\$_" + } + } + + $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units) + } + finally { + foreach ($kvp in $originalEnv.GetEnumerator()) { + [System.Environment]::SetEnvironmentVariable($kvp.Key, $kvp.Value, "Process") + } + } + + if ($compile.Errors.HasErrors) { + $msg = "Failed to compile C# code: " + foreach ($e in $compile.Errors) { + $msg += "`r`n" + $e.ToString() + } + throw [InvalidOperationException]$msg + } + $compiled_assembly = $compile.CompiledAssembly + } + + $type_accelerator = [PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators") + foreach ($accelerator in $type_accelerators) { + $type_name = $accelerator.TypeName + $found = $false + + foreach ($assembly_type in $compiled_assembly.GetTypes()) { + if ($assembly_type.Name -eq $type_name) { + $type_accelerator::Add($accelerator.Name, $assembly_type) + $found = $true + break + } + } + if (-not $found) { + throw "Failed to find compiled class '$type_name' for custom TypeAccelerator." + } + } + + # return the compiled assembly if PassThru is set. + if ($PassThru) { + return $compiled_assembly + } +} + +Export-ModuleMember -Function Add-CSharpType diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/shell_plugins/pwsh.py b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/shell_plugins/pwsh.py index 468c54069..9b1601046 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/shell_plugins/pwsh.py +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_sqlserver_test_plugins/shell_plugins/pwsh.py @@ -59,10 +59,15 @@ import re import shlex import pkgutil import xml.etree.ElementTree as ET -import ntpath + +from packaging import version from ansible.module_utils._text import to_bytes, to_text from ansible.plugins.shell import ShellBase +from ansible.release import __version__ as ansible_version +from ansible.utils.display import Display + +display = Display() _common_args = ['pwsh', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Unrestricted'] @@ -223,18 +228,12 @@ class ShellModule(ShellBase): def build_module_command(self, env_string, shebang, cmd, arg_path=None): bootstrap_wrapper = pkgutil.get_data("ansible.executor.powershell", "bootstrap_wrapper.ps1") - # pipelining bypass - if cmd == '': - return self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False) - - # non-pipelining - - cmd_parts = shlex.split(cmd, posix=False) - cmd_parts = list(map(to_text, cmd_parts)) - if shebang and shebang.lower() == '#!powershell': - if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'): - # we're running a module via the bootstrap wrapper - cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0]) + # TODO: remove when support for ansible-core <2.13 is dropped + ver = version.parse(ansible_version) + cutoff = version.parse('2.13') + info = "ansible_version (parsed) [<2.13]: %s (%s) [%r]" % (ansible_version, ver, (ver < cutoff)) + display.vvv(info) + if ver < cutoff: # HACK begin dirty, dirty hack # we need to override the built-in Ansible.Basic module util # to one that will work on non-Windows platforms. @@ -246,8 +245,8 @@ class ShellModule(ShellBase): # before it made it to the remote host. The reason we can't just embed it in commands as strings is because # it will be too big. local_mu = os.path.join(os.path.dirname(__file__), '..', 'module_utils') - ansible_basic_cs = os.path.join(local_mu, 'Ansible.Basic.cs') - addtype_ps = os.path.join(local_mu, 'Ansible.ModuleUtils.AddType.psm1') + ansible_basic_cs = os.path.join(local_mu, '_Ansible.Basic.cs') + addtype_ps = os.path.join(local_mu, '_Ansible.ModuleUtils.AddType.psm1') wrapper_hacked = ''' &chcp.com 65001 > $null $exec_wrapper_str = $input | Out-String @@ -286,6 +285,21 @@ class ShellModule(ShellBase): &$exec_wrapper ''' % (ansible_basic_cs, addtype_ps) bootstrap_wrapper = wrapper_hacked + # end hack for ansible-core < 2.13 + + # pipelining bypass + if cmd == '': + return self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False) + + # non-pipelining + + cmd_parts = shlex.split(cmd, posix=False) + cmd_parts = list(map(to_text, cmd_parts)) + if shebang and shebang.lower() == '#!powershell': + if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'): + # we're running a module via the bootstrap wrapper + cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0]) + wrapper_cmd = "cat " + cmd_parts[0] + " | " + self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False) return wrapper_cmd elif shebang and shebang.startswith('#!'): diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_win_sqlserver/tasks/main.yml b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_win_sqlserver/tasks/main.yml index acc1dbeb4..0928ecc1c 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_win_sqlserver/tasks/main.yml +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/setup_win_sqlserver/tasks/main.yml @@ -2,7 +2,6 @@ - name: Install Powershell modules ansible.windows.win_shell: | {{ item }} - no_log: "{{ ansible_verbosity | int < 3 }}" loop: - - "{{ dbatools_install_cmd }}" - "{{ dbops_install_cmd }}" + - "{{ dbatools_install_cmd }}" diff --git a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/win_ag_replica/tasks/main.yml b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/win_ag_replica/tasks/main.yml index 8a8990789..feb3acb55 100644 --- a/ansible_collections/lowlydba/sqlserver/tests/integration/targets/win_ag_replica/tasks/main.yml +++ b/ansible_collections/lowlydba/sqlserver/tests/integration/targets/win_ag_replica/tasks/main.yml @@ -56,6 +56,10 @@ lowlydba.sqlserver.availability_group: register: ag + - name: Set ag SqlInstance fact + set_fact: + ag_sql_instance: "{{ ag.data.SqlInstance }}" + - name: Set replica lowlydba.sqlserver.ag_replica: session_timeout: 20 @@ -72,7 +76,6 @@ - result.data.AvailabilityMode == availability_mode - result.data.SessionTimeout == 20 - result.data.FailoverMode == failover_mode - - result.data.EndpointUrl == "TCP://{{ ag.data.SqlInstance }}:5022" - result is changed always: -- cgit v1.2.3