From 501acae5abaa81ed29670a26b367c54ebe611d53 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 7 Nov 2018 10:53:17 +1000 Subject: [PATCH] Added basic equivalent to PowerShell modules (#44705) * Added basic equivalent to PowerShell modules * changes based on latest review * Added tests * ignore sanity test due to how tests are set up * Changes to work with PSCore * Added documentation and change updated more modules * Add some speed optimisations to AddType * fix some issues in the doc changes * doc changes --- .../fragments/powershell_basic_util.yaml | 2 + .../developing_modules_general_windows.rst | 195 +- lib/ansible/executor/module_common.py | 3 +- .../module_utils/csharp/Ansible.Basic.cs | 1242 +++++++++++ .../Ansible.ModuleUtils.AddType.psm1 | 100 +- .../modules/windows/win_certificate_store.ps1 | 141 +- .../modules/windows/win_environment.ps1 | 80 +- lib/ansible/modules/windows/win_ping.ps1 | 23 +- lib/ansible/modules/windows/win_uri.ps1 | 157 +- .../win_certificate_store/tasks/test.yml | 12 +- .../targets/win_csharp_utils/aliases | 2 + .../library/ansible_basic_tests.ps1 | 1938 +++++++++++++++++ .../targets/win_csharp_utils/tasks/main.yml | 9 + .../targets/win_environment/tasks/main.yml | 4 +- .../targets/win_ping/tasks/main.yml | 31 - test/sanity/pslint/ignore.txt | 1 + test/sanity/validate-modules/main.py | 24 +- 17 files changed, 3647 insertions(+), 317 deletions(-) create mode 100644 changelogs/fragments/powershell_basic_util.yaml create mode 100644 lib/ansible/module_utils/csharp/Ansible.Basic.cs create mode 100644 test/integration/targets/win_csharp_utils/aliases create mode 100644 test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 create mode 100644 test/integration/targets/win_csharp_utils/tasks/main.yml diff --git a/changelogs/fragments/powershell_basic_util.yaml b/changelogs/fragments/powershell_basic_util.yaml new file mode 100644 index 00000000000..7e5ebd5e8d3 --- /dev/null +++ b/changelogs/fragments/powershell_basic_util.yaml @@ -0,0 +1,2 @@ +minor_changes: +- Added Ansible.Basic C# util that contains a module wrapper and handles common functions like argument parsing and module return. This is gives the user more visibility over what the module has run and aligns PowerShell modules more closely to how Python modules are defined. diff --git a/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst b/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst index f5c96f35987..fb7d00a495a 100644 --- a/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst +++ b/docs/docsite/rst/dev_guide/developing_modules_general_windows.rst @@ -166,17 +166,18 @@ Windows new module development When creating a new module there are a few things to keep in mind: - Module code is in Powershell (.ps1) files while the documentation is contained in Python (.py) files of the same name -- Avoid using ``Write-Host/Debug/Verbose/Error`` in the module and add what needs to be returned to the ``$result`` variable -- When trying an exception use ``Fail-Json -obj $result -message "exception message here"`` instead +- Avoid using ``Write-Host/Debug/Verbose/Error`` in the module and add what needs to be returned to the ``$module.Result`` variable +- To fail a module, call ``$module.FailJson("failure message here")``, an Exception or ErrorRecord can be set to the second argument for a more descriptive error message - Most new modules require check mode and integration tests before they are merged into the main Ansible codebase - Avoid using try/catch statements over a large code block, rather use them for individual calls so the error message can be more descriptive - Try and catch specific exceptions when using try/catch statements - Avoid using PSCustomObjects unless necessary - Look for common functions in ``./lib/ansible/module_utils/powershell/`` and use the code there instead of duplicating work. These can be imported by adding the line ``#Requires -Module *`` where * is the filename to import, and will be automatically included with the module code sent to the Windows target when run via Ansible +- As well as PowerShell module utils, C# module utils are stored in ``./lib/ansible/module_utils/csharp/`` and are automatically imported in a module execution if the line ``#AnsibleRequires -CSharpUtil *`` is present +- C# and PowerShell module utils achieve the same goal but C# allows a developer to implement low level tasks, such as calling the Win32 API, and can be faster in some cases - Ensure the code runs under Powershell v3 and higher on Windows Server 2008 and higher; if higher minimum Powershell or OS versions are required, ensure the documentation reflects this clearly - Ansible runs modules under strictmode version 2.0. Be sure to test with that enabled by putting ``Set-StrictMode -Version 2.0`` at the top of your dev script - Favour native Powershell cmdlets over executable calls if possible -- If adding an object to ``$result``, ensure any trailing slashes are removed or escaped, as ``ConvertTo-Json`` will fail to convert it - Use the full cmdlet name instead of aliases, e.g. ``Remove-Item`` over ``rm`` - Use named parameters with cmdlets, e.g. ``Remove-Item -Path C:\temp`` over ``Remove-Item C:\temp`` @@ -190,6 +191,62 @@ A very basic powershell module `win_environment `_ which additionally shows how to use different parameter types (bool, str, int, list, dict, path) and a selection of choices for parameters, how to fail a module and how to handle exceptions. +As part of the new ``AnsibleModule`` wrapper, the input parameters are defined and validated based on an argument +spec. The following options can be set at the root level of the argument spec: + +- ``mutually_exclusive``: A list of lists, where the inner list contains module options that cannot be set together +- ``no_log``: Stops the module from emitting any logs to the Windows Event log +- ``options``: A dictionary where the key is the module option and the value is the spec for that option +- ``required``: Will fail when the module option is not set +- ``required_if``: A list of lists where the inner list contains 3 or 4 elements; + * The first element is the module option to check the value against + * The second element is the value of the option specified by the first element, if matched then the required if check is run + * The third element is a list of required module options when the above is matched + * An optional fourth element is a boolean that states whether all module options in the third elements are required (default: ``$false``) or only one (``$true``) +- ``required_one_of``: A list of lists, where the inner list contains module options where at least one must be set +- ``required_together``: A list of lists, where the inner list contains module options that must be set together +- ``supports_check_mode``: Whether the module supports check mode, by default this is ``$false`` + +The actual input options for a module are set within the ``options`` value as a dictionary. The keys of this dictionary +are the module option names while the values are the spec of that module option. Each spec can have the following +options set: + +- ``aliases``: A list of aliases for the module option +- ``choices``: A list of valid values for the module option, if ``type=list`` then each list value is validated against the choices and not the list itself +- ``default``: The default value for the module option if not set +- ``elements``: When ``type=list``, this sets the type of each list value, the values are the same as ``type`` +- ``no_log``: Will sanitise the input value before being returned in the ``module_invocation`` return value +- ``removed_in_version``: States when a deprecated module option is to be removed, a warning is displayed to the end user if set +- ``type``: The type of the module option, if not set then it defaults to ``str``. The valid types are; + * ``bool``: A boolean value + * ``dict``: A dictionary value, if the input is a JSON or key=value string then it is converted to dictionary + * ``float``: A float or `Single `_ value + * ``int``: An Int32 value + * ``json``: A string where the value is converted to a JSON string if the input is a dictionary + * ``list``: A list of values, ``elements=`` can convert the individual list value types if set. If ``elements=dict`` then ``options`` is defined, the values will be validated against the argument spec. When the input is a string then the string is split by ``,`` and any whitespace is trimmed + * ``path``: A string where values likes ``%TEMP%`` are expanded based on environment values. If the input value starts with ``\\?\`` then no expansion is run + * ``raw``: No conversions occur on the value passed in by Ansible + * ``sid``: Will convert Windows security identifier values or Windows account names to a `SecurityIdentifier `_ value + * ``str``: The value is converted to a string + +When ``type=dict``, or ``type=list`` and ``elements=dict``, the following keys can also be set for that module option: + +- ``apply_defaults``: The value is based on the ``options`` spec defaults for that key if ``True`` and null if ``False``. Only valid when the module option is not defined by the user and ``type=dict``. +- ``mutually_exclusive``: Same as the root level ``mutually_exclusive`` but validated against the values in the sub dict +- ``options``: Same as the root level ``options`` but contains the valid options for the sub option +- ``required_if``: Same as the root level ``required_if`` but validated against the values in the sub dict +- ``required_one_of``: Same as the root level ``required_one_of`` but validated against the values in the sub dict + +A module type can also be a delegate function that converts the value to whatever is required by the module option. For +example the following snippet shows how to create a custom type that creates a ``UInt64`` value: + +.. code-block:: powershell + + $spec = @{ + uint64_type = @{ type = [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0]) } } + } + $uint64_type = $module.Params.uint64_type + When in doubt, look at some of the other core modules and see how things have been implemented there. @@ -216,6 +273,11 @@ These are the checks that can be used within Ansible modules: - ``#Requires -Version x.y``: Added in Ansible 2.5, specifies the version of PowerShell that is required by the module. The module will fail if this requirement is not met. - ``#AnsibleRequires -OSVersion x.y``: Added in Ansible 2.5, specifies the OS build version that is required by the module and will fail if this requirement is not met. The actual OS version is derived from ``[Environment]::OSVersion.Version``. - ``#AnsibleRequires -Become``: Added in Ansible 2.5, forces the exec runner to run the module with ``become``, which is primarily used to bypass WinRM restrictions. If ``ansible_become_user`` is not specified then the ``SYSTEM`` account is used instead. +- ``#AnsibleRequires -CSharpUtil Ansible.``: Added in Ansible 2.8, specifies a C# module_util to load in for the module execution. + +C# module utils can reference other C# utils by adding the line +``using Ansible.;`` to the top of the script with all the other +using statements. Windows module utilities @@ -230,7 +292,56 @@ can be imported by adding the following line to a PowerShell module: #Requires -Module Ansible.ModuleUtils.Legacy This will import the module_util at ``./lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1`` -and enable calling all of its functions. +and enable calling all of its functions. As of Ansible 2.8, Windows module +utils can also be written in C# and stored at ``lib/ansible/module_utils/csharp``. +These module_utils can be imported by adding the following line to a PowerShell +module: + +.. code-block:: powershell + + #AnsibleRequires -CSharpUtil Ansible.Basic + +This will import the module_util at ``./lib/ansible/module_utils/csharp/Ansible.Basic.cs`` +and automatically load the types in the executing process. C# module utils can +reference each other and be loaded together by adding the following line to the +using statements at the top of the util: + +.. code-block:: csharp + + using Ansible.Become; + +There are special comments that can be set in a C# file for controlling the +compilation parameters. The following comments can be added to the script; + +- ``//AssemblyReference -Name [-CLR [Core|Framework]]``: The assembly DLL to reference during compilation, the optional ``-CLR`` flag can also be used to state whether to reference when running under .NET Core, Framework, or both (if omitted) +- ``//NoWarn -Name [-CLR [Core|Framework]]``: A compiler warning ID to ignore when compiling the code, the optional ``-CLR`` works the same as above. A list of warnings can be found at `Compiler errors `_ + +As well as this, the following pre-processor symbols are defined; + +- ``CORECLR``: This symbol is present when PowerShell is running through .NET Core +- ``WINDOWS``: This symbol is present when PowerShell is running on Windows +- ``UNIX``: This symbol is present when PowerShell is running on Unix + +A combination of these flags help to make a module util interoperable on both +.NET Framework and .NET Core, here is an example of them in action: + +.. code-block:: csharp + + #if CORECLR + using Newtonsoft.Json; + #else + using System.Web.Script.Serialization; + #endif + + //AssemblyReference -Name Newtonsoft.Json.dll -CLR Core + //AssemblyReference -Name System.Web.Extensions.dll -CLR Framework + + // Ignore error CS1702 for all .NET types + //NoWarn -Name CS1702 + + // Ignore error CS1956 only for .NET Framework + //NoWarn -Name CS1956 -CLR Framework + The following is a list of module_utils that are packaged with Ansible and a general description of what they do: @@ -251,9 +362,13 @@ distribution for use with custom modules. Custom module_utils are placed in a folder called ``module_utils`` located in the root folder of the playbook or role directory. -The below example is a role structure that contains two custom module_utils -called ``Ansible.ModuleUtils.ModuleUtil1`` and -``Ansible.ModuleUtils.ModuleUtil2``:: +C# module utilities can also be stored outside of the standard Ansible distribution for use with custom modules. Like +PowerShell utils, these are stored in a folder called ``module_utils`` and the filename must end in the extension +``.cs``, start with ``Ansible.`` and be named after the namespace defined in the util. + +The below example is a role structure that contains two PowerShell custom module_utils called +``Ansible.ModuleUtils.ModuleUtil1``, ``Ansible.ModuleUtils.ModuleUtil2``, and a C# util containing the namespace +``Ansible.CustomUtil``:: meta/ main.yml @@ -262,15 +377,16 @@ called ``Ansible.ModuleUtils.ModuleUtil1`` and module_utils/ Ansible.ModuleUtils.ModuleUtil1.psm1 Ansible.ModuleUtils.ModuleUtil2.psm1 + Ansible.CustomUtil.cs tasks/ main.yml -Each module_util must contain at least one function, and a list of functions, aliases and cmdlets to export for use -in a module. This can be a blanket export by using ``*``. For example: +Each PowerShell module_util must contain at least one function that has been exported with ``Export-ModuleMember`` +at the end of the file. For example .. code-block:: powershell - Export-ModuleMember -Alias * -Function * -Cmdlet * + Export-ModuleMember -Function Invoke-CustomUtil, Get-CustomInfo Windows playbook module testing @@ -306,34 +422,53 @@ useful when developing a new module or implementing bug fixes. These are some steps that need to be followed to set this up: - Copy the module script to the Windows server -- Copy ``./lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1`` to the same directory as the script above -- To stop the script from exiting the editor on a successful run, in ``Ansible.ModuleUtils.Legacy.psm1`` under the function ``Exit-Json``, replace the last two lines of the function with:: - - ConvertTo-Json -InputObject $obj -Depth 99 +- Copy the folders ``./lib/ansible/module_utils/powershell`` and ``./lib/ansible/module_utils/csharp`` to the same directory as the script above +- Add an extra ``#`` to the start of any ``#Requires -Module`` lines in the module code +- Add the following to the start of the module script that was copied to the server: -- To stop the script from exiting the editor on a failed run, in ``Ansible.ModuleUtils.Legacy.psm1`` under the function ``Fail-Json``, replace the last two lines of the function with:: +.. code-block:: powershell - Write-Error -Message (ConvertTo-Json -InputObject $obj -Depth 99) + # Set $ErrorActionPreference to what's set during Ansible execution + $ErrorActionPreference = "Stop" -- Add the following to the start of the module script that was copied to the server:: + # Set the first argument file to a JSON that contains the module args + $args = @("$($pwd.Path)\args.json") - ### start setup code + # Or instead of an args file, set $complex_args to the pre-processed module args $complex_args = @{ - "_ansible_check_mode" = $false - "_ansible_diff" = $false - "path" = "C:\temp" - "state" = "present" + _ansible_check_mode = $false + _ansible_diff = $false + path = "C:\temp" + state = "present" } - Import-Module -Name .\Ansible.ModuleUtils.Legacy.psm1 - ### end setup code - -You can add more args to ``$complex_args`` as required by the module. The -module can now be run on the Windows host either directly through Powershell -or through an IDE. + # Import any C# utils referenced with '#AnsibleRequires -CSharpUtil' or 'using Ansible.; + Import-Module -Name "$($pwd.Path)\powershell\Ansible.ModuleUtils.AddType.psm1" + $_csharp_utils = @( + "$($pwd.Path)\csharp\Ansible.Basic.cs" + ) + Add-CSharpType -References $_csharp_utils -IncludeDebugInfo + + # Import any PowerShell modules referenced with '#Requires -Module` + Import-Module -Name "$($pwd.Path)\powershell\Ansible.ModuleUtils.Legacy.psm1" + + # End of the setup code and start of the module code + #!powershell + +You can add more args to ``$complex_args`` as required by the module or define the module options through a JSON file +with the structure:: + + { + "ANSIBLE_MODULE_ARGS": { + "_ansible_check_mode": false, + "_ansible_diff": false, + "path": "C:\\temp", + "state": "present" + } + } There are multiple IDEs that can be used to debug a Powershell script, two of -the most popular are +the most popular ones are - `Powershell ISE`_ - `Visual Studio Code`_ @@ -348,7 +483,7 @@ these steps. - Log onto the Windows server using the same user account that Ansible used to execute the module. - Navigate to ``%TEMP%\..``. It should contain a folder starting with ``ansible-tmp-``. - Inside this folder, open the PowerShell script for the module. -- In this script is a raw JSON script under ``$json_raw`` which contains the module arguments under ``module_args``. These args can be assigned manually to the ``$complex_args`` variable that is defined on your debug script. +- In this script is a raw JSON script under ``$json_raw`` which contains the module arguments under ``module_args``. These args can be assigned manually to the ``$complex_args`` variable that is defined on your debug script or put in the ``args.json`` file. Windows unit testing diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py index 0d19c005d6e..f3f3006e948 100644 --- a/lib/ansible/executor/module_common.py +++ b/lib/ansible/executor/module_common.py @@ -649,7 +649,8 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas b_module_data = b_module_data.replace(REPLACER_WINDOWS, b'#Requires -Module Ansible.ModuleUtils.Legacy') elif re.search(b'#Requires -Module', b_module_data, re.IGNORECASE) \ or re.search(b'#Requires -Version', b_module_data, re.IGNORECASE)\ - or re.search(b'#AnsibleRequires -OSVersion', b_module_data, re.IGNORECASE): + or re.search(b'#AnsibleRequires -OSVersion', b_module_data, re.IGNORECASE) \ + or re.search(b'#AnsibleRequires -CSharpUtil', b_module_data, re.IGNORECASE): module_style = 'new' module_substyle = 'powershell' elif REPLACER_JSONARGS in b_module_data: diff --git a/lib/ansible/module_utils/csharp/Ansible.Basic.cs b/lib/ansible/module_utils/csharp/Ansible.Basic.cs new file mode 100644 index 00000000000..16a7bc972c7 --- /dev/null +++ b/lib/ansible/module_utils/csharp/Ansible.Basic.cs @@ -0,0 +1,1242 @@ +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 -Name Newtonsoft.Json.dll -CLR Core +//AssemblyReference -Name System.ComponentModel.Primitives.dll -CLR Core +//AssemblyReference -Name System.Diagnostics.EventLog.dll -CLR Core +//AssemblyReference -Name System.IO.FileSystem.AccessControl.dll -CLR Core +//AssemblyReference -Name System.Security.Principal.Windows.dll -CLR Core +//AssemblyReference -Name System.Security.AccessControl.dll -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); + + 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 }, + { "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 } }, + { "elements", new List() { null, null } }, + { "mutually_exclusive", new List() { typeof(List>), null } }, + { "no_log", new List() { false, typeof(bool) } }, + { "options", new List() { typeof(Hashtable), typeof(Hashtable) } }, + { "removed_in_version", new List() { null, typeof(string) } }, + { "required", new List() { false, typeof(bool) } }, + { "required_if", new List() { typeof(List>), null } }, + { "required_one_of", new List() { typeof(List>), null } }, + { "required_together", new List() { typeof(List>), null } }, + { "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) + { + // 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); + Params = GetParams(args); + aliases = GetAliases(argumentSpec, Params); + SetNoLogValues(argumentSpec, Params); + } + catch (Exception e) + { + 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) + { + return new AnsibleModule(args, argumentSpec); + } + + public void Debug(string message) + { + if (DebugMode) + LogEvent(String.Format("[DEBUG] {0}", message)); + } + + public void Deprecate(string message, string version) + { + deprecations.Add(new Dictionary() { { "message", message }, { "version", version } }); + LogEvent(String.Format("[DEPRECATION WARNING] {0} {1}", message, version)); + } + + 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; + + 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) + { + Warn(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; + eventLog.WriteEntry(message, logEntryType, 0); + } + } + + public void Warn(string message) + { + warnings.Add(message); + LogEvent(String.Format("[WARNING] {0}", message), EventLogEntryType.Warning); + } + + public static Dictionary 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) + { +#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)); + if (optionsContext.Count > 0) + msg += String.Format(" - found in {0}.", String.Join(" -> ", optionsContext)); + throw new ArgumentException(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]; + + bool isArray = actualType.IsArray && (actualType.GetElementType() == entryType || actualType.GetElementType() == typeof(object)); + if (actualType == entryType || isArray) + { + object[] rawArray; + if (isArray) + rawArray = (object[])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); + if (optionsContext.Count > 0) + msg += String.Format(" - found in {0}.", String.Join(" -> ", optionsContext)); + throw new ArgumentException(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); + if (optionsContext.Count > 0) + msg += String.Format(" - found in {0}", String.Join(" -> ", optionsContext)); + msg += String.Format(". Valid types are: {0}", 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); + if (optionsContext.Count > 0) + msg += String.Format(" - found in {0}", String.Join(" -> ", optionsContext)); + throw new ArgumentException(msg); + } + } + } + + // Outside of the spec iterator, change the values that were casted above + foreach (KeyValuePair changedValue in changedValues) + argumentSpec[changedValue.Key] = changedValue.Value; + + // Now make sure all the metadata keys are set to their defaults + 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; + } + } + + 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]; + } + } + + 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; + if (noLogObject != null) + noLogValues.Add(noLogObject.ToString()); + } + + 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()); + } + } + + 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); + + if (CheckMode && !(bool)spec["supports_check_mode"]) + { + 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); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(msg); + } + + // ensure it matches the choices if there are choices set + List choices = (List)v["choices"]; + if (choices.Count > 0) + { + // allow one or more when type="list" param with choices + if (type == "list") + { + var diffList = ((List)value).Except(choices).ToList(); + if (diffList.Count > 0) + { + string msg = String.Format("value of {0} must be one or more of: {1}. Got no match for: {2}", + k, String.Join(", ", choices), String.Join(", ", diffList)); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(msg); + } + } + else if (!choices.Contains(value)) + { + string msg = String.Format("value of {0} must be one of: {1}, got: {2}", k, String.Join(", ", choices), value); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(msg); + } + } + } + } + + CheckRequiredTogether(param, (IList)spec["required_together"]); + CheckRequiredOneOf(param, (IList)spec["required_one_of"]); + CheckRequiredIf(param, (IList)spec["required_if"]); + + // 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(); + List removedParameters = new List(); + + foreach (DictionaryEntry entry in param) + { + string paramKey = (string)entry.Key; + if (!legalInputs.Contains(paramKey)) + unsupportedParameters.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)); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + msg += String.Format(". Supported parameters include: {0}", String.Join(", ", legalInputs)); + FailJson(msg); + } + } + + 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)); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(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)); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(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)); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(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)); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(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)); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(msg); + } + } + } + + private void CheckSubOption(IDictionary param, string key, IDictionary spec) + { + string type; + if (spec["type"].GetType() == typeof(string)) + type = (string)spec["type"]; + else + type = "delegate"; + string elements = (string)spec["elements"]; + object value = param[key]; + + 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(); + Delegate typeConverter = optionTypes[elements]; + 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); + if (optionsContext.Count > 0) + msg += String.Format(" found in {0}", String.Join(" -> ", optionsContext)); + FailJson(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, new String('*', omitMe.Length)); + 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(); + } + + [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) + { + // When running over psrp there may not be a console to write the + // line to, we check if there is a Console Window and fallback on + // setting a variable that the Ansible module_wrapper will check + // and output to the PowerShell Output stream on close. + if (GetConsoleWindow() != IntPtr.Zero) + Console.WriteLine(line); + else + ScriptBlock.Create("Set-Variable -Name _ansible_output -Value $args[0] -Scope Global").Invoke(line); + } + } +} + diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 index a6a521bfcab..ab062cbee80 100644 --- a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 @@ -19,9 +19,8 @@ Function Add-CSharpType { [Switch] Whether to return the loaded Assembly .PARAMETER AnsibleModule - TODO - This is an AnsibleModule object that is used to derive the - TempPath and Debug values. - TempPath is set to the TmpDir property of the class + [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 @@ -69,8 +68,8 @@ Function Add-CSharpType { } # pattern used to find referenced assemblies in the code - $assembly_pattern = "^//\s*AssemblyReference\s+-Name\s+(?[\w.]*)(\s+-CLR\s+(?Core|Framework))?$" - $no_warn_pattern = "^//\s*NoWarn\s+-Name\s+(?[\w\d]*)(\s+-CLR\s+(?Core|Framework))?$" + $assembly_pattern = [Regex]"//\s*AssemblyReference\s+-Name\s+(?[\w.]*)(\s+-CLR\s+(?Core|Framework))?" + $no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?[\w\d]*)(\s+-CLR\s+(?Core|Framework))?" # PSCore vs PSDesktop use different methods to compile the code, # PSCore uses Roslyn and can compile the code purely in memory @@ -99,29 +98,26 @@ Function Add-CSharpType { foreach ($reference in $References) { # scan through code and add any assemblies that match # //AssemblyReference -Name ... [-CLR Core] - $sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference - try { - while ($null -ne ($line = $sr.ReadLine())) { - if ($line -imatch $assembly_pattern) { - # verify the reference is not for .NET Framework - if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") { - continue - } - $assembly_path = $Matches.Name - 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 - } - if ($line -imatch $no_warn_pattern) { - if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") { - continue - } - $ignore_warnings.Add($Matches.Name, [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress) - } + # //NoWarn -Name ... [-CLR Core] + $assembly_matches = $assembly_pattern.Matches($reference) + foreach ($match in $assembly_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Core") { + continue + } + $assembly_path = $match.Groups["Name"] + if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) { + $assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path } - } finally { - $sr.Close() + $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 } @@ -203,7 +199,7 @@ Function Add-CSharpType { # configure compile options based on input if ($PSCmdlet.ParameterSetName -eq "Module") { - $temp_path = $AnsibleModule.TmpDir + $temp_path = $AnsibleModule.Tmpdir $include_debug = $AnsibleModule.Verbosity -ge 3 } else { $temp_path = $TempPath @@ -231,34 +227,32 @@ Function Add-CSharpType { # create a code snippet for each reference and check if we need # to reference any extra assemblies - # //AssemblyReference -Name ... [-CLR Framework] $ignore_warnings = [System.Collections.ArrayList]@() $compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@() foreach ($reference in $References) { - $sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference - try { - while ($null -ne ($line = $sr.ReadLine())) { - if ($line -imatch $assembly_pattern) { - # verify the reference is not for .NET Core - if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") { - continue - } - $assemblies.Add($Matches.Name) > $null - } - if ($line -imatch $no_warn_pattern) { - if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") { - continue - } - $warning_id = $Matches.Name - # /nowarn should only contain the numeric part - if ($warning_id.StartsWith("CS")) { - $warning_id = $warning_id.Substring(2) - } - $ignore_warnings.Add($warning_id) > $null - } + # scan through code and add any assemblies that match + # //AssemblyReference -Name ... [-CLR Framework] + # //NoWarn -Name ... [-CLR Framework] + $assembly_matches = $assembly_pattern.Matches($reference) + foreach ($match in $assembly_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Framework") { + continue + } + $assemblies.Add($match.Groups["Name"].Value) > $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) } - } finally { - $sr.Close() + $ignore_warnings.Add($warning_id) > $null } $compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null } @@ -270,7 +264,7 @@ Function Add-CSharpType { # compile the code together and check for errors $provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider - $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units.ToArray()) + $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units) if ($compile.Errors.HasErrors) { $msg = "Failed to compile C# code: " foreach ($e in $compile.Errors) { diff --git a/lib/ansible/modules/windows/win_certificate_store.ps1 b/lib/ansible/modules/windows/win_certificate_store.ps1 index 31d27068236..39af21bbb4e 100644 --- a/lib/ansible/modules/windows/win_certificate_store.ps1 +++ b/lib/ansible/modules/windows/win_certificate_store.ps1 @@ -3,35 +3,36 @@ # Copyright: (c) 2017, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -#Requires -Module Ansible.ModuleUtils.Legacy +#AnsibleRequires -CSharpUtil Ansible.Basic -$ErrorActionPreference = "Stop" +$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues() | ForEach-Object { $_.ToString() } +$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() } -$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues() -$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() - -$params = Parse-Args $args -supports_check_mode $true -$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false - -$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "exported", "present" -$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty ($state -eq "present" -or $state -eq "exported") -$thumbprint = Get-AnsibleParam -obj $params -name "thumbprint" -type "str" -failifempty ($state -eq "exported") -$store_name = Get-AnsibleParam -obj $params -name "store_name" -type "str" -default "My" -validateset $store_name_values -$store_location = Get-AnsibleParam -obj $params -name "store_location" -type "str" -default "LocalMachine" -validateset $store_location_values -$password = Get-AnsibleParam -obj $params -name "password" -type "str" -$key_exportable = Get-AnsibleParam -obj $params -name "key_exportable" -type "bool" -default $true -$key_storage = Get-AnsibleParam -obj $params -name "key_storage" -type "str" -default "default" -validateset "default", "machine", "user" -$file_type = Get-AnsibleParam -obj $params -name "file_type" -type "str" -default "der" -validateset "der", "pem", "pkcs12" - -$result = @{ - changed = $false - thumbprints = @() +$spec = @{ + options = @{ + state = @{ type = "str"; default = "present"; choices = "absent", "exported", "present" } + path = @{ type = "path" } + thumbprint = @{ type = "str" } + store_name = @{ type = "str"; default = "My"; choices = $store_name_values } + store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values } + password = @{ type = "str"; no_log = $true } + key_exportable = @{ type = "bool"; default = $true } + key_storage = @{ type = "str"; default = "default"; choices = "default", "machine", "user" } + file_type = @{ type = "str"; default = "der"; choices = "der", "pem", "pkcs12" } + } + required_if = @( + @("state", "absent", @("path", "thumbprint"), $true), + @("state", "exported", @("path", "thumbprint")), + @("state", "present", @("path")) + ) + supports_check_mode = $true } +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) -Function Get-CertFile($path, $password, $key_exportable, $key_storage) { +Function Get-CertFile($module, $path, $password, $key_exportable, $key_storage) { # parses a certificate file and returns X509Certificate2Collection if (-not (Test-Path -Path $path -PathType Leaf)) { - Fail-Json -obj $result -message "File at '$path' either does not exist or is not a file" + $module.FailJson("File at '$path' either does not exist or is not a file") } # must set at least the PersistKeySet flag so that the PrivateKey @@ -52,13 +53,13 @@ Function Get-CertFile($path, $password, $key_exportable, $key_storage) { try { $certs.Import($path, $password, $store_flags) } catch { - Fail-Json -obj $result -message "Failed to load cert from file: $($_.Exception.Message)" + $module.FailJson("Failed to load cert from file: $($_.Exception.Message)", $_) } return $certs } -Function New-CertFile($cert, $path, $type, $password) { +Function New-CertFile($module, $cert, $path, $type, $password) { $content_type = switch ($type) { "pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert } "der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert } @@ -72,20 +73,20 @@ Function New-CertFile($cert, $path, $type, $password) { $missing_key = $true } if ($missing_key) { - Fail-Json -obj $result -message "Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accesible by the current user" + $module.FailJson("Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accesible by the current user") } } if (Test-Path -Path $path) { Remove-Item -Path $path -Force - $result.changed = $true + $module.Result.changed = $true } try { $cert_bytes = $cert.Export($content_type, $password) } catch { - Fail-Json -obj $result -message "Failed to export certificate as bytes: $($_.Exception.Message)" + $module.FailJson("Failed to export certificate as bytes: $($_.Exception.Message)", $_) } - + # Need to manually handle a PEM file if ($type -eq "pem") { $cert_content = "-----BEGIN CERTIFICATE-----`r`n" @@ -95,26 +96,26 @@ Function New-CertFile($cert, $path, $type, $password) { $file_encoding = [System.Text.Encoding]::ASCII $cert_bytes = $file_encoding.GetBytes($cert_content) } elseif ($type -eq "pkcs12") { - $result.key_exported = $false + $module.Result.key_exported = $false if ($cert.PrivateKey -ne $null) { - $result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable + $module.Result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable } } - - if (-not $check_mode) { + + if (-not $module.CheckMode) { try { [System.IO.File]::WriteAllBytes($path, $cert_bytes) } catch [System.ArgumentNullException] { - Fail-Json -obj $result -message "Failed to write cert to file, cert was null: $($_.Exception.Message)" + $module.FailJson("Failed to write cert to file, cert was null: $($_.Exception.Message)", $_) } catch [System.IO.IOException] { - Fail-Json -obj $result -message "Failed to write cert to file due to IO exception: $($_.Exception.Message)" + $module.FailJson("Failed to write cert to file due to IO Exception: $($_.Exception.Message)", $_) } catch [System.UnauthorizedAccessException, System>Security.SecurityException] { - Fail-Json -obj $result -message "Failed to write cert to file due to permission: $($_.Exception.Message)" + $module.FailJson("Failed to write cert to file due to permissions: $($_.Exception.Message)", $_) } catch { - Fail-Json -obj $result -message "Failed to write cert to file: $($_.Exception.Message)" + $module.FailJson("Failed to write cert to file: $($_.Exception.Message)", $_) } } - $result.changed = $true + $module.Result.changed = $true } Function Get-CertFileType($path, $password) { @@ -147,70 +148,78 @@ Function Get-CertFileType($path, $password) { } } -$store_name = [System.Security.Cryptography.X509Certificates.StoreName]::$store_name -$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]::$store_location +$state = $module.Params.state +$path = $module.Params.path +$thumbprint = $module.Params.thumbprint +$store_name = [System.Security.Cryptography.X509Certificates.StoreName]"$($module.Params.store_name)" +$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)" +$password = $module.Params.password +$key_exportable = $module.Params.key_exportable +$key_storage = $module.Params.key_storage +$file_type = $module.Params.file_type + +$module.Result.thumbprints = @() + $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location try { $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) } catch [System.Security.Cryptography.CryptographicException] { - Fail-Json -obj $result -message "Unable to open the store as it is not readable: $($_.Exception.Message)" + $module.FailJson("Unable to open the store as it is not readable: $($_.Exception.Message)", $_) } catch [System.Security.SecurityException] { - Fail-Json -obj $result -message "Unable to open the store with the current permissions: $($_.Exception.Message)" + $module.FailJson("Unable to open the store with the current permissions: $($_.Exception.Message)", $_) } catch { - Fail-Json -obj $result -message "Unable to open the store: $($_.Exception.Message)" + $module.FailJson("Unable to open the store: $($_.Exception.Message)", $_) } $store_certificates = $store.Certificates try { if ($state -eq "absent") { $cert_thumbprints = @() - + if ($path -ne $null) { - $certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage + $certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage foreach ($cert in $certs) { $cert_thumbprints += $cert.Thumbprint } } elseif ($thumbprint -ne $null) { $cert_thumbprints += $thumbprint - } else { - Fail-Json -obj $result -message "Either path or thumbprint must be set when state=absent" } foreach ($cert_thumbprint in $cert_thumbprints) { - $result.thumbprints += $cert_thumbprint + $module.Result.thumbprints += $cert_thumbprint $found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert_thumbprint, $false) if ($found_certs.Count -gt 0) { foreach ($found_cert in $found_certs) { try { - if (-not $check_mode) { + if (-not $module.CheckMode) { $store.Remove($found_cert) } } catch [System.Security.SecurityException] { - Fail-Json -obj $result -message "Unable to remove cert with thumbprint '$cert_thumbprint' with the current permissions: $($_.Exception.Message)" + $module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint' with current permissions: $($_.Exception.Message)", $_) } catch { - Fail-Json -obj $result -message "Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)" + $module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)", $_) } - $result.changed = $true + $module.Result.changed = $true } } } } elseif ($state -eq "exported") { # TODO: Add support for PKCS7 and exporting a cert chain - $result.thumbprints += $thumbprint + $module.Result.thumbprints += $thumbprint $export = $true if (Test-Path -Path $path -PathType Container) { - Fail-Json -obj $result -message "Cannot export cert to path '$path' as it is a directory" + $module.FailJson("Cannot export cert to path '$path' as it is a directory") } elseif (Test-Path -Path $path -PathType Leaf) { $actual_cert_type = Get-CertFileType -path $path -password $password if ($actual_cert_type -eq $file_type) { try { - $certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage + $certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage } catch { # failed to load the file so we set the thumbprint to something # that will fail validation $certs = @{Thumbprint = $null} } - + if ($certs.Thumbprint -eq $thumbprint) { $export = $false } @@ -220,27 +229,27 @@ try { if ($export) { $found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false) if ($found_certs.Count -ne 1) { - Fail-Json -obj $result -message "Found $($found_certs.Count) certs when only expecting 1" + $module.FailJson("Found $($found_certs.Count) certs when only expecting 1") } - - New-CertFile -cert $found_certs -path $path -type $file_type -password $password + + New-CertFile -module $module -cert $found_certs -path $path -type $file_type -password $password } } else { - $certs = Get-CertFile -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage + $certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage foreach ($cert in $certs) { - $result.thumbprints += $cert.Thumbprint + $module.Result.thumbprints += $cert.Thumbprint $found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false) if ($found_certs.Count -eq 0) { try { - if (-not $check_mode) { + if (-not $module.CheckMode) { $store.Add($cert) } } catch [System.Security.Cryptography.CryptographicException] { - Fail-Json -obj $result -message "Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)" + $module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)", $_) } catch { - Fail-Json -obj $result -message "Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)" + $module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)", $_) } - $result.changed = $true + $module.Result.changed = $true } } } @@ -248,4 +257,4 @@ try { $store.Close() } -Exit-Json -obj $result +$module.ExitJson() diff --git a/lib/ansible/modules/windows/win_environment.ps1 b/lib/ansible/modules/windows/win_environment.ps1 index 5c2ce2cc1a4..53e28e68aa4 100644 --- a/lib/ansible/modules/windows/win_environment.ps1 +++ b/lib/ansible/modules/windows/win_environment.ps1 @@ -3,67 +3,59 @@ # Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -#Requires -Module Ansible.ModuleUtils.Legacy - -$ErrorActionPreference = "Stop" +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + name = @{ type = "str"; required = $true } + level = @{ type = "str"; choices = "machine", "process", "user"; required = $true } + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + value = @{ type = "str" } + } + required_if = @(,@("state", "present", @("value"))) + supports_check_mode = $true +} -$params = Parse-Args -arguments $args -supports_check_mode $true -$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false -$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) -$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true -$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present" -$value = Get-AnsibleParam -obj $params -name "value" -type "str" -$level = Get-AnsibleParam -obj $params -name "level" -type "str" -validateSet "machine","user","process" -failifempty $true +$name = $module.Params.name +$level = $module.Params.level +$state = $module.Params.state +$value = $module.Params.value $before_value = [Environment]::GetEnvironmentVariable($name, $level) - -$result = @{ - before_value = $before_value - changed = $false - value = $value -} +$module.Result.before_value = $before_value +$module.Result.value = $value # When removing environment, set value to $null if set if ($state -eq "absent" -and $value) { - Add-Warning -obj $result -message "When removing environment variable '$name' it should not have a value '$value' set" + $module.Warn("When removing environment variable '$name' it should not have a value '$value' set") $value = $null } elseif ($state -eq "present" -and (-not $value)) { - Fail-Json -obj $result -message "When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent" + $module.FailJson("When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent") } -if ($state -eq "present" -and $before_value -ne $value) { +$module.Diff.before = @{ $level = @{} } +if ($before_value) { + $module.Diff.before.$level.$name = $before_value +} +$module.Diff.after = @{ $level = @{} } +if ($value) { + $module.Diff.after.$level.$name = $value +} - if (-not $check_mode) { +if ($state -eq "present" -and $before_value -ne $value) { + if (-not $module.CheckMode) { [Environment]::SetEnvironmentVariable($name, $value, $level) } - $result.changed = $true - - if ($diff_mode) { - if ($before_value -eq $null) { - $result.diff = @{ - prepared = " [$level]`n+$name = $value`n" - } - } else { - $result.diff = @{ - prepared = " [$level]`n-$name = $before_value`n+$name = $value`n" - } - } - } + $module.Result.changed = $true } elseif ($state -eq "absent" -and $before_value -ne $null) { - - if (-not $check_mode) { + if (-not $module.CheckMode) { [Environment]::SetEnvironmentVariable($name, $null, $level) } - $result.changed = $true - - if ($diff_mode) { - $result.diff = @{ - prepared = " [$level]`n-$name = $before_value`n" - } - } - + $module.Result.changed = $true } -Exit-Json -obj $result +$module.ExitJson() + diff --git a/lib/ansible/modules/windows/win_ping.ps1 b/lib/ansible/modules/windows/win_ping.ps1 index 898eed607e4..c848b9121ea 100644 --- a/lib/ansible/modules/windows/win_ping.ps1 +++ b/lib/ansible/modules/windows/win_ping.ps1 @@ -2,21 +2,20 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -#Requires -Module Ansible.ModuleUtils.Legacy +#AnsibleRequires -CSharpUtil Ansible.Basic -$ErrorActionPreference = "Stop" - -$params = Parse-Args $args -supports_check_mode $true - -$data = Get-AnsibleParam -obj $params -name "data" -type "str" -default "pong" +$spec = @{ + options = @{ + data = @{ type = "str"; default = "pong" } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$data = $module.Params.data if ($data -eq "crash") { throw "boom" } -$result = @{ - changed = $false - ping = $data -} - -Exit-Json $result +$module.Result.ping = $data +$module.ExitJson() diff --git a/lib/ansible/modules/windows/win_uri.ps1 b/lib/ansible/modules/windows/win_uri.ps1 index 7c5ea736df1..b78a6b1bbee 100644 --- a/lib/ansible/modules/windows/win_uri.ps1 +++ b/lib/ansible/modules/windows/win_uri.ps1 @@ -4,65 +4,80 @@ # Copyright: (c) 2017, Dag Wieers (@dagwieers) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -#Requires -Module Ansible.ModuleUtils.Legacy +#AnsibleRequires -CSharpUtil Ansible.Basic #Requires -Module Ansible.ModuleUtils.CamelConversion #Requires -Module Ansible.ModuleUtils.FileUtil +#Requires -Module Ansible.ModuleUtils.Legacy -$ErrorActionPreference = "Stop" - -$params = Parse-Args -arguments $args -supports_check_mode $true -$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false - -$url = Get-AnsibleParam -obj $params -name "url" -type "str" -failifempty $true -$method = Get-AnsibleParam -obj $params "method" -type "str" -default "GET" -validateset "CONNECT","DELETE","GET","HEAD","MERGE","OPTIONS","PATCH","POST","PUT","REFRESH","TRACE" -$content_type = Get-AnsibleParam -obj $params -name "content_type" -type "str" -$headers = Get-AnsibleParam -obj $params -name "headers" -$body = Get-AnsibleParam -obj $params -name "body" -$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" - -$user = Get-AnsibleParam -obj $params -name "user" -type "str" -$password = Get-AnsibleParam -obj $params -name "password" -type "str" -$force_basic_auth = Get-AnsibleParam -obj $params -name "force_basic_auth" -type "bool" -default $false - -$creates = Get-AnsibleParam -obj $params -name "creates" -type "path" -$removes = Get-AnsibleParam -obj $params -name "removes" -type "path" - -$follow_redirects = Get-AnsibleParam -obj $params -name "follow_redirects" -type "str" -default "safe" -validateset "all","none","safe" -$maximum_redirection = Get-AnsibleParam -obj $params -name "maximum_redirection" -type "int" -default 50 -$return_content = Get-AnsibleParam -obj $params -name "return_content" -type "bool" -default $false -$status_code = Get-AnsibleParam -obj $params -name "status_code" -type "list" -default @(200) -$timeout = Get-AnsibleParam -obj $params -name "timeout" -type "int" -default 30 -$validate_certs = Get-AnsibleParam -obj $params -name "validate_certs" -type "bool" -default $true -$client_cert = Get-AnsibleParam -obj $params -name "client_cert" -type "path" -$client_cert_password = Get-AnsibleParam -obj $params -name "client_cert_password" -type "str" +$spec = @{ + options = @{ + url = @{ type = "str"; required = $true } + method = @{ + type = "str" + default = "GET" + choices = "CONNECT", "DELETE", "GET", "HEAD", "MERGE", "OPTIONS", "PATCH", "POST", "PUT", "REFRESH", "TRACE" + } + content_type = @{ type = "str" } + headers = @{ type = "dict" } + body = @{ type = "raw" } + dest = @{ type = "path" } + user = @{ type = "str" } + password = @{ type = "str"; no_log = $true } + force_basic_auth = @{ type = "bool"; default = $false } + creates = @{ type = "path" } + removes = @{ type = "path" } + follow_redirects = @{ + type = "str" + default = "safe" + choices = "all", "none", "safe" + } + maximum_redirection = @{ type = "int"; default = 50 } + return_content = @{ type = "bool"; default = $false } + status_code = @{ type = "list"; elements = "int"; default = @(200) } + timeout = @{ type = "int"; default = 30 } + validate_certs = @{ type = "bool"; default = $true } + client_cert = @{ type = "path" } + client_cert_password = @{ type = "str"; no_log = $true } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$url = $module.Params.url +$method = $module.Params.method +$content_type = $module.Params.content_type +$headers = $module.Params.headers +$body = $module.Params.body +$dest = $module.Params.dest +$user = $module.Params.user +$password = $module.Params.password +$force_basic_auth = $module.Params.force_basic_auth +$creates = $module.Params.creates +$removes = $module.Params.removes +$follow_redirects = $module.Params.follow_redirects +$maximum_redirection = $module.Params.maximum_redirection +$return_content = $module.Params.return_content +$status_code = $module.Params.status_code +$timeout = $module.Params.timeout +$validate_certs = $module.Params.validate_certs +$client_cert = $module.Params.client_cert +$client_cert_password = $module.Params.client_cert_password $JSON_CANDIDATES = @('text', 'json', 'javascript') -$result = @{ - changed = $false - elapsed = 0 - url = $url -} +$module.Result.elapsed = 0 +$module.Result.url = $url if ($creates -and (Test-AnsiblePath -Path $creates)) { - $result.skipped = $true - Exit-Json -obj $result -message "The 'creates' file or directory ($creates) already exists." + $module.Result.skipped = $true + $module.Result.msg = "The 'creates' file or directory ($creates) already exists." + $module.ExitJson() } if ($removes -and -not (Test-AnsiblePath -Path $removes)) { - $result.skipped = $true - Exit-Json -obj $result -message "The 'removes' file or directory ($removes) does not exist." -} - -if ($status_code) { - $status_code = foreach ($code in $status_code) { - try { - [int]$code - } - catch [System.InvalidCastException] { - Fail-Json -obj $result -message "Failed to convert '$code' to an integer. Status codes must be provided in numeric format." - } - } + $module.Result.skipped = $true + $module.Result.msg = "The 'removes' file or directory ($removes) does not exist." + $module.ExitJson() } # Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5) @@ -114,7 +129,7 @@ if ($headers) { $req_headers = New-Object -TypeName System.Net.WebHeaderCollection foreach ($header in $headers.GetEnumerator()) { # some headers need to be set on the property itself - switch ($header.Name) { + switch ($header.Key) { Accept { $client.Accept = $header.Value } Connection { $client.Connection = $header.Value } Content-Length { $client.ContentLength = $header.Value } @@ -130,7 +145,7 @@ if ($headers) { $client.TransferEncoding = $header.Value } User-Agent { $client.UserAgent = $header.Value } - default { $req_headers.Add($header.Name, $header.Value) } + default { $req_headers.Add($header.Key, $header.Value) } } } $client.Headers.Add($req_headers) @@ -138,15 +153,15 @@ if ($headers) { if ($client_cert) { if (-not (Test-AnsiblePath -Path $client_cert)) { - Fail-Json -obj $result -message "Client certificate '$client_cert' does not exist" + $module.FailJson("Client certificate '$client_cert' does not exit") } try { $certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection -ArgumentList $client_cert, $client_cert_password $client.ClientCertificates = $certs } catch [System.Security.Cryptography.CryptographicException] { - Fail-Json -obj $result -message "Failed to read client certificate '$client_cert': $($_.Exception.Message)" + $module.FailJson("Failed to read client certificate '$client_cert': $($_.Exception.Message)", $_) } catch { - Fail-Json -obj $result -message "Unhandled exception when reading client certificate at '$client_cert': $($_.Exception.Message)" + $module.FailJson("Unhandled exception when reading client certificate at '$client_cert': $($_.Exception.Message)", $_) } } @@ -160,7 +175,7 @@ if ($user -and $password) { $client.Credentials = $credential } } elseif ($user -or $password) { - Add-Warning -obj $result -message "Both 'user' and 'password' parameters are required together, skipping authentication" + $module.Warn("Both 'user' and 'password' parameters are required together, skipping authentication") } if ($null -ne $body) { @@ -187,7 +202,7 @@ $module_start = Get-Date try { $response = $client.GetResponse() } catch [System.Net.WebException] { - $result.elapsed = ((Get-Date) - $module_start).TotalSeconds + $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds $response = $null if ($_.Exception.PSObject.Properties.Name -match "Response") { # was a non-successful response but we at least have a response and @@ -198,17 +213,17 @@ try { # in the case a response (or empty response) was on the exception like in # a timeout scenario, we should still fail if ($null -eq $response) { - $result.elapsed = ((Get-Date) - $module_start).TotalSeconds - Fail-Json -obj $result -message "WebException occurred when sending web request: $($_.Exception.Message)" + $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds + $module.FailJson("WebException occurred when sending web request: $($_.Exception.Message)", $_) } } catch [System.Net.ProtocolViolationException] { - $result.elapsed = ((Get-Date) - $module_start).TotalSeconds - Fail-Json -obj $result -message "ProtocolViolationException when sending web request: $($_.Exception.Message)" + $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds + $module.FailJson("ProtocolViolationException when sending web request: $($_.Exception.Message)", $_) } catch { - $result.elapsed = ((Get-Date) - $module_start).TotalSeconds - Fail-Json -obj $result -message "Unhandled exception occured when sending web request. Exception: $($_.Exception.Message)" + $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds + $module.FailJson("Unhandled exception occured when sending web request. Exception: $($_.Exception.Message)", $_) } -$result.elapsed = ((Get-Date) - $module_start).TotalSeconds +$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds ForEach ($prop in $response.psobject.properties) { $result_key = Convert-StringToSnakeCase -string $prop.Name @@ -217,7 +232,7 @@ ForEach ($prop in $response.psobject.properties) { if ($prop_value -is [System.DateTime]) { $prop_value = $prop_value.ToString("o", [System.Globalization.CultureInfo]::InvariantCulture) } - $result.$result_key = $prop_value + $module.Result.$result_key = $prop_value } # manually get the headers as not all of them are in the response properties @@ -225,7 +240,7 @@ foreach ($header_key in $response.Headers.GetEnumerator()) { $header_value = $response.Headers[$header_key] $header_key = $header_key.Replace("-", "") # replace - with _ for snake conversion $header_key = Convert-StringToSnakeCase -string $header_key - $result.$header_key = $header_value + $module.Result.$header_key = $header_value } # we only care about the return body if we need to return the content or create a file @@ -241,10 +256,10 @@ if ($return_content -or $dest) { if ($return_content) { $memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null $content_bytes = $memory_st.ToArray() - $result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes) - if ($result.ContainsKey("content_type") -and $result.content_type -Match ($JSON_CANDIDATES -join '|')) { + $module.Result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes) + if ($module.Result.ContainsKey("content_type") -and $module.Result.content_type -Match ($JSON_CANDIDATES -join '|')) { try { - $result.json = ConvertFrom-Json -InputObject $result.content + $module.Result.json = ([Ansible.Basic.AnsibleModule]::FromJson($module.Result.content)) } catch [System.ArgumentException] { # Simply continue, since 'text' might be anything } @@ -266,8 +281,8 @@ if ($return_content -or $dest) { } } - $result.changed = $changed - if ($changed -and (-not $check_mode)) { + $module.Result.changed = $changed + if ($changed -and (-not $module.CheckMode)) { $memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null $file_stream = [System.IO.File]::Create($dest) try { @@ -284,7 +299,7 @@ if ($return_content -or $dest) { } if ($status_code -notcontains $response.StatusCode) { - Fail-Json -obj $result -message "Status code of request '$([int]$response.StatusCode)' is not in list of valid status codes $status_code : '$($response.StatusCode)'." + $module.FailJson("Status code of request '$([int]$response.StatusCode)' is not in list of valid status codes $status_code : $($response.StatusCode)'.") } -Exit-Json -obj $result +$module.ExitJson() diff --git a/test/integration/targets/win_certificate_store/tasks/test.yml b/test/integration/targets/win_certificate_store/tasks/test.yml index eb2ac5ad854..eac2039b833 100644 --- a/test/integration/targets/win_certificate_store/tasks/test.yml +++ b/test/integration/targets/win_certificate_store/tasks/test.yml @@ -5,7 +5,7 @@ path: '{{win_cert_dir}}\subj-cert.pem' store_location: FakeLocation register: fail_fake_location - failed_when: "fail_fake_location.msg != 'Get-AnsibleParam: Argument store_location needs to be one of CurrentUser,LocalMachine but was FakeLocation.'" + failed_when: "fail_fake_location.msg != 'value of store_location must be one of: CurrentUser, LocalMachine, got: FakeLocation'" - name: fail with invalid store name win_certificate_store: @@ -13,27 +13,27 @@ path: '{{win_cert_dir}}\subj-cert.pem' store_name: FakeName register: fail_fake_name - failed_when: "fail_fake_name.msg != 'Get-AnsibleParam: Argument store_name needs to be one of AddressBook,AuthRoot,CertificateAuthority,Disallowed,My,Root,TrustedPeople,TrustedPublisher but was FakeName.'" + failed_when: "fail_fake_name.msg != 'value of store_name must be one of: AddressBook, AuthRoot, CertificateAuthority, Disallowed, My, Root, TrustedPeople, TrustedPublisher, got: FakeName'" - name: fail when state=present and no path is set win_certificate_store: state: present register: fail_present_no_path - failed_when: "fail_present_no_path.msg != 'Get-AnsibleParam: Missing required argument: path'" + failed_when: "fail_present_no_path.msg != 'state is present but all of the following are missing: path'" - name: fail when state=exported and no path is set win_certificate_store: state: exported thumbprint: ABC register: fail_export_no_path - failed_when: "fail_export_no_path.msg != 'Get-AnsibleParam: Missing required argument: path'" + failed_when: "fail_export_no_path.msg != 'state is exported but all of the following are missing: path'" - name: fail when state=exported and no thumbprint is set win_certificate_store: state: exported path: '{{win_cert_dir}}' register: fail_export_no_thumbprint - failed_when: "fail_export_no_thumbprint.msg != 'Get-AnsibleParam: Missing required argument: thumbprint'" + failed_when: "fail_export_no_thumbprint.msg != 'state is exported but all of the following are missing: thumbprint'" - name: fail to export thumbprint when path is a dir win_certificate_store: @@ -47,7 +47,7 @@ win_certificate_store: state: absent register: fail_absent_no_path_or_thumbprint - failed_when: fail_absent_no_path_or_thumbprint.msg != 'Either path or thumbprint must be set when state=absent' + failed_when: "fail_absent_no_path_or_thumbprint.msg != 'state is absent but any of the following are missing: path, thumbprint'" - name: import pem certificate (check) win_certificate_store: diff --git a/test/integration/targets/win_csharp_utils/aliases b/test/integration/targets/win_csharp_utils/aliases new file mode 100644 index 00000000000..1eed2ecfaf4 --- /dev/null +++ b/test/integration/targets/win_csharp_utils/aliases @@ -0,0 +1,2 @@ +shippable/windows/group1 +shippable/windows/smoketest diff --git a/test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 b/test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 new file mode 100644 index 00000000000..1caf0ae69d8 --- /dev/null +++ b/test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 @@ -0,0 +1,1938 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) + +Function Assert-Equals { + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual, + [Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected + ) + + $matched = $false + if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) { + $Actual.Count | Assert-Equals -Expected $Expected.Count + for ($i = 0; $i -lt $Actual.Count; $i++) { + $actual_value = $Actual[$i] + $expected_value = $Expected[$i] + Assert-Equals -Actual $actual_value -Expected $expected_value + } + $matched = $true + } else { + $matched = $Actual -ceq $Expected + } + + if (-not $matched) { + if ($Actual -is [PSObject]) { + $Actual = $Actual.ToString() + } + + $call_stack = (Get-PSCallStack)[1] + $module.Result.failed = $true + $module.Result.test = $test + $module.Result.actual = $Actual + $module.Result.expected = $Expected + $module.Result.line = $call_stack.ScriptLineNumber + $module.Result.method = $call_stack.Position.Text + $module.Result.msg = "AssertionError: actual != expected" + + Exit-Module + } +} + +Function Assert-DictionaryEquals { + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual, + [Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected + ) + $actual_keys = $Actual.Keys + $expected_keys = $Expected.Keys + + $actual_keys.Count | Assert-Equals -Expected $expected_keys.Count + foreach ($actual_entry in $Actual.GetEnumerator()) { + $actual_key = $actual_entry.Key + ($actual_key -cin $expected_keys) | Assert-Equals -Expected $true + $actual_value = $actual_entry.Value + $expected_value = $Expected.$actual_key + + if ($actual_value -is [System.Collections.IDictionary]) { + $actual_value | Assert-DictionaryEquals -Expected $expected_value + } elseif ($actual_value -is [System.Collections.ArrayList]) { + for ($i = 0; $i -lt $actual_value.Count; $i++) { + $actual_entry = $actual_value[$i] + $expected_entry = $expected_value[$i] + if ($actual_entry -is [System.Collections.IDictionary]) { + $actual_entry | Assert-DictionaryEquals -Expected $expected_entry + } else { + Assert-Equals -Actual $actual_entry -Expected $expected_entry + } + } + } else { + Assert-Equals -Actual $actual_value -Expected $expected_value + } + } + foreach ($expected_key in $expected_keys) { + ($expected_key -cin $actual_keys) | Assert-Equals -Expected $true + } +} + +Function Exit-Module { + # Make sure Exit actually calls exit and not our overriden test behaviour + [Ansible.Basic.AnsibleModule]::Exit = { param([Int32]$rc) exit $rc } + Write-Output -InputObject (ConvertTo-Json -InputObject $module.Result -Compress -Depth 99) + $module.ExitJson() +} + +$tmpdir = $module.Tmpdir + +# Override the Exit and WriteLine behaviour to throw an exception instead of exiting the module +[Ansible.Basic.AnsibleModule]::Exit = { + param([Int32]$rc) + throw "exit: $rc" +} +[Ansible.Basic.AnsibleModule]::WriteLine = { + param([String]$line) + Set-Variable -Name _test_out -Scope Global -Value $line +} + +$tests = @{ + "Empty spec and no options - args file" = { + $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json" + [System.IO.File]::WriteAllText($args_file, '{ "ANSIBLE_MODULE_ARGS": {} }') + $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{}) + + $m.CheckMode | Assert-Equals -Expected $false + $m.DebugMode | Assert-Equals -Expected $false + $m.DiffMode | Assert-Equals -Expected $false + $m.KeepRemoteFiles | Assert-Equals -Expected $false + $m.ModuleName | Assert-Equals -Expected "undefined win module" + $m.NoLog | Assert-Equals -Expected $false + $m.Verbosity | Assert-Equals -Expected 0 + $m.AnsibleVersion | Assert-Equals -Expected $null + } + + "Empty spec and no options - complex_args" = { + $complex_args = @{} + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + + $m.CheckMode | Assert-Equals -Expected $false + $m.DebugMode | Assert-Equals -Expected $false + $m.DiffMode | Assert-Equals -Expected $false + $m.KeepRemoteFiles | Assert-Equals -Expected $false + $m.ModuleName | Assert-Equals -Expected "undefined win module" + $m.NoLog | Assert-Equals -Expected $false + $m.Verbosity | Assert-Equals -Expected 0 + $m.AnsibleVersion | Assert-Equals -Expected $null + } + + "Internal param changes - args file" = { + $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)" + New-Item -Path $m_tmpdir -ItemType Directory > $null + $args_file = Join-Path -Path $tmpdir -ChildPath "args-$(Get-Random).json" + [System.IO.File]::WriteAllText($args_file, @" +{ + "ANSIBLE_MODULE_ARGS": { + "_ansible_check_mode": true, + "_ansible_debug": true, + "_ansible_diff": true, + "_ansible_keep_remote_files": true, + "_ansible_module_name": "ansible_basic_tests", + "_ansible_no_log": true, + "_ansible_remote_tmp": "%TEMP%", + "_ansible_selinux_special_fs": "ignored", + "_ansible_shell_executable": "ignored", + "_ansible_socket": "ignored", + "_ansible_syslog_facility": "ignored", + "_ansible_tmpdir": "$($m_tmpdir -replace "\\", "\\")", + "_ansible_verbosity": 3, + "_ansible_version": "2.8.0" + } +} +"@) + $m = [Ansible.Basic.AnsibleModule]::Create(@($args_file), @{supports_check_mode=$true}) + $m.CheckMode | Assert-Equals -Expected $true + $m.DebugMode | Assert-Equals -Expected $true + $m.DiffMode | Assert-Equals -Expected $true + $m.KeepRemoteFiles | Assert-Equals -Expected $true + $m.ModuleName | Assert-Equals -Expected "ansible_basic_tests" + $m.NoLog | Assert-Equals -Expected $true + $m.Verbosity | Assert-Equals -Expected 3 + $m.AnsibleVersion | Assert-Equals -Expected "2.8.0" + $m.Tmpdir | Assert-Equals -Expected $m_tmpdir + } + + "Internal param changes - complex_args" = { + $m_tmpdir = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)" + New-Item -Path $m_tmpdir -ItemType Directory > $null + $complex_args = @{ + _ansible_check_mode = $true + _ansible_debug = $true + _ansible_diff = $true + _ansible_keep_remote_files = $true + _ansible_module_name = "ansible_basic_tests" + _ansible_no_log = $true + _ansible_remote_tmp = "%TEMP%" + _ansible_selinux_special_fs = "ignored" + _ansible_shell_executable = "ignored" + _ansible_socket = "ignored" + _ansible_syslog_facility = "ignored" + _ansible_tmpdir = $m_tmpdir.ToString() + _ansible_verbosity = 3 + _ansible_version = "2.8.0" + } + $spec = @{ + supports_check_mode = $true + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + $m.CheckMode | Assert-Equals -Expected $true + $m.DebugMode | Assert-Equals -Expected $true + $m.DiffMode | Assert-Equals -Expected $true + $m.KeepRemoteFiles | Assert-Equals -Expected $true + $m.ModuleName | Assert-Equals -Expected "ansible_basic_tests" + $m.NoLog | Assert-Equals -Expected $true + $m.Verbosity | Assert-Equals -Expected 3 + $m.AnsibleVersion | Assert-Equals -Expected "2.8.0" + $m.Tmpdir | Assert-Equals -Expected $m_tmpdir + } + + "Parse complex module options" = { + $spec = @{ + options = @{ + option_default = @{} + missing_option_default = @{} + string_option = @{type = "str"} + required_option = @{required = $true} + missing_choices = @{choices = "a", "b"} + choices = @{choices = "a", "b"} + one_choice = @{choices = ,"b"} + choice_with_default = @{choices = "a", "b"; default = "b"} + alias_direct = @{aliases = ,"alias_direct1"} + alias_as_alias = @{aliases = "alias_as_alias1", "alias_as_alias2"} + bool_type = @{type = "bool"} + bool_from_str = @{type = "bool"} + dict_type = @{ + type = "dict" + options = @{ + int_type = @{type = "int"} + str_type = @{type = "str"; default = "str_sub_type"} + } + } + dict_type_missing = @{ + type = "dict" + options = @{ + int_type = @{type = "int"} + str_type = @{type = "str"; default = "str_sub_type"} + } + } + dict_type_defaults = @{ + type = "dict" + apply_defaults = $true + options = @{ + int_type = @{type = "int"} + str_type = @{type = "str"; default = "str_sub_type"} + } + } + dict_type_json = @{type = "dict"} + dict_type_str = @{type = "dict"} + float_type = @{type = "float"} + int_type = @{type = "int"} + json_type = @{type = "json"} + json_type_dict = @{type = "json"} + list_type = @{type = "list"} + list_type_str = @{type = "list"} + list_with_int = @{type = "list"; elements = "int"} + list_type_single = @{type = "list"} + list_with_dict = @{ + type = "list" + elements = "dict" + options = @{ + int_type = @{type = "int"} + str_type = @{type = "str"; default = "str_sub_type"} + } + } + path_type = @{type = "path"} + path_type_nt = @{type = "path"} + path_type_missing = @{type = "path"} + raw_type_str = @{type = "raw"} + raw_type_int = @{type = "raw"} + sid_type = @{type = "sid"} + sid_from_name = @{type = "sid"} + str_type = @{type = "str"} + delegate_type = @{type = [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0]) }} + } + } + $complex_args = @{ + option_default = 1 + string_option = 1 + required_option = "required" + choices = "a" + one_choice = "b" + alias_direct = "a" + alias_as_alias2 = "a" + bool_type = $true + bool_from_str = "false" + dict_type = @{ + int_type = "10" + } + dict_type_json = '{"a":"a","b":1,"c":["a","b"]}' + dict_type_str = 'a=a b="b 2" c=c' + float_type = "3.14159" + int_type = 0 + json_type = '{"a":"a","b":1,"c":["a","b"]}' + json_type_dict = @{ + a = "a" + b = 1 + c = @("a", "b") + } + list_type = @("a", "b", 1, 2) + list_type_str = "a, b,1,2 " + list_with_int = @("1", 2) + list_type_single = "single" + list_with_dict = @( + @{ + int_type = 2 + str_type = "dict entry" + }, + @{ int_type = 1 }, + @{} + ) + path_type = "%SystemRoot%\System32" + path_type_nt = "\\?\%SystemRoot%\System32" + path_type_missing = "T:\missing\path" + raw_type_str = "str" + raw_type_int = 1 + sid_type = "S-1-5-18" + sid_from_name = "SYSTEM" + str_type = "str" + delegate_type = "1234" + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + + $m.Params.option_default | Assert-Equals -Expected "1" + $m.Params.option_default.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.missing_option_default | Assert-Equals -Expected $null + $m.Params.string_option | Assert-Equals -Expected "1" + $m.Params.string_option.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.required_option | Assert-Equals -Expected "required" + $m.Params.required_option.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.missing_choices | Assert-Equals -Expected $null + $m.Params.choices | Assert-Equals -Expected "a" + $m.Params.choices.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.one_choice | Assert-Equals -Expected "b" + $m.Params.one_choice.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.choice_with_default | Assert-Equals -Expected "b" + $m.Params.choice_with_default.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.alias_direct | Assert-Equals -Expected "a" + $m.Params.alias_direct.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.alias_as_alias | Assert-Equals -Expected "a" + $m.Params.alias_as_alias.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.bool_type | Assert-Equals -Expected $true + $m.Params.bool_type.GetType().ToString() | Assert-Equals -Expected "System.Boolean" + $m.Params.bool_from_str | Assert-Equals -Expected $false + $m.Params.bool_from_str.GetType().ToString() | Assert-Equals -Expected "System.Boolean" + $m.Params.dict_type | Assert-DictionaryEquals -Expected @{int_type = 10; str_type = "str_sub_type"} + $m.Params.dict_type.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]" + $m.Params.dict_type.int_type.GetType().ToString() | Assert-Equals -Expected "System.Int32" + $m.Params.dict_type.str_type.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.dict_type_missing | Assert-Equals -Expected $null + $m.Params.dict_type_defaults | Assert-DictionaryEquals -Expected @{int_type = $null; str_type = "str_sub_type"} + $m.Params.dict_type_defaults.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]" + $m.Params.dict_type_defaults.str_type.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.dict_type_json | Assert-DictionaryEquals -Expected @{ + a = "a" + b = 1 + c = @("a", "b") + } + $m.Params.dict_type_json.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]" + $m.Params.dict_type_json.a.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.dict_type_json.b.GetType().ToString() | Assert-Equals -Expected "System.Int32" + $m.Params.dict_type_json.c.GetType().ToString() | Assert-Equals -Expected "System.Collections.ArrayList" + $m.Params.dict_type_str | Assert-DictionaryEquals -Expected @{a = "a"; b = "b 2"; c = "c"} + $m.Params.dict_type_str.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.Dictionary``2[System.String,System.Object]" + $m.Params.dict_type_str.a.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.dict_type_str.b.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.dict_type_str.c.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.float_type | Assert-Equals -Expected ([System.Single]3.14159) + $m.Params.float_type.GetType().ToString() | Assert-Equals -Expected "System.Single" + $m.Params.int_type | Assert-Equals -Expected 0 + $m.Params.int_type.GetType().ToString() | Assert-Equals -Expected "System.Int32" + $m.Params.json_type | Assert-Equals -Expected '{"a":"a","b":1,"c":["a","b"]}' + $m.Params.json_type.GetType().ToString() | Assert-Equals -Expected "System.String" + [Ansible.Basic.AnsibleModule]::FromJson($m.Params.json_type_dict) | Assert-DictionaryEquals -Expected ([Ansible.Basic.AnsibleModule]::FromJson('{"a":"a","b":1,"c":["a","b"]}')) + $m.Params.json_type_dict.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.list_type.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]" + $m.Params.list_type.Count | Assert-Equals -Expected 4 + $m.Params.list_type[0] | Assert-Equals -Expected "a" + $m.Params.list_type[0].GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.list_type[1] | Assert-Equals -Expected "b" + $m.Params.list_type[1].GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.list_type[2] | Assert-Equals -Expected 1 + $m.Params.list_type[2].GetType().FullName | Assert-Equals -Expected "System.Int32" + $m.Params.list_type[3] | Assert-Equals -Expected 2 + $m.Params.list_type[3].GetType().FullName | Assert-Equals -Expected "System.Int32" + $m.Params.list_type_str.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]" + $m.Params.list_type_str.Count | Assert-Equals -Expected 4 + $m.Params.list_type_str[0] | Assert-Equals -Expected "a" + $m.Params.list_type_str[0].GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.list_type_str[1] | Assert-Equals -Expected "b" + $m.Params.list_type_str[1].GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.list_type_str[2] | Assert-Equals -Expected "1" + $m.Params.list_type_str[2].GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.list_type_str[3] | Assert-Equals -Expected "2" + $m.Params.list_type_str[3].GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.list_with_int.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]" + $m.Params.list_with_int.Count | Assert-Equals -Expected 2 + $m.Params.list_with_int[0] | Assert-Equals -Expected 1 + $m.Params.list_with_int[0].GetType().FullName | Assert-Equals -Expected "System.Int32" + $m.Params.list_with_int[1] | Assert-Equals -Expected 2 + $m.Params.list_with_int[1].GetType().FullName | Assert-Equals -Expected "System.Int32" + $m.Params.list_type_single.GetType().ToString() | Assert-Equals -Expected "System.Collections.Generic.List``1[System.Object]" + $m.Params.list_type_single.Count | Assert-Equals -Expected 1 + $m.Params.list_type_single[0] | Assert-Equals -Expected "single" + $m.Params.list_type_single[0].GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.list_with_dict.GetType().FullName.StartsWith("System.Collections.Generic.List``1[[System.Object") | Assert-Equals -Expected $true + $m.Params.list_with_dict.Count | Assert-Equals -Expected 3 + $m.Params.list_with_dict[0].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equals -Expected $true + $m.Params.list_with_dict[0] | Assert-DictionaryEquals -Expected @{int_type = 2; str_type = "dict entry"} + $m.Params.list_with_dict[0].int_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.Int32" + $m.Params.list_with_dict[0].str_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.String" + $m.Params.list_with_dict[1].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equals -Expected $true + $m.Params.list_with_dict[1] | Assert-DictionaryEquals -Expected @{int_type = 1; str_type = "str_sub_type"} + $m.Params.list_with_dict[1].int_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.Int32" + $m.Params.list_with_dict[1].str_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.String" + $m.Params.list_with_dict[2].GetType().FullName.StartsWith("System.Collections.Generic.Dictionary``2[[System.String") | Assert-Equals -Expected $true + $m.Params.list_with_dict[2] | Assert-DictionaryEquals -Expected @{int_type = $null; str_type = "str_sub_type"} + $m.Params.list_with_dict[2].str_type.GetType().FullName.ToString() | Assert-Equals -Expected "System.String" + $m.Params.path_type | Assert-Equals -Expected "$($env:SystemRoot)\System32" + $m.Params.path_type.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.path_type_nt | Assert-Equals -Expected "\\?\%SystemRoot%\System32" + $m.Params.path_type_nt.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.path_type_missing | Assert-Equals -Expected "T:\missing\path" + $m.Params.path_type_missing.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.raw_type_str | Assert-Equals -Expected "str" + $m.Params.raw_type_str.GetType().FullName | Assert-Equals -Expected "System.String" + $m.Params.raw_type_int | Assert-Equals -Expected 1 + $m.Params.raw_type_int.GetType().FullName | Assert-Equals -Expected "System.Int32" + $m.Params.sid_type | Assert-Equals -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18") + $m.Params.sid_type.GetType().ToString() | Assert-Equals -Expected "System.Security.Principal.SecurityIdentifier" + $m.Params.sid_from_name | Assert-Equals -Expected (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList "S-1-5-18") + $m.Params.sid_from_name.GetType().ToString() | Assert-Equals -Expected "System.Security.Principal.SecurityIdentifier" + $m.Params.str_type | Assert-Equals -Expected "str" + $m.Params.str_type.GetType().ToString() | Assert-Equals -Expected "System.String" + $m.Params.delegate_type | Assert-Equals -Expected 1234 + $m.Params.delegate_type.GetType().ToString() | Assert-Equals -Expected "System.UInt64" + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_module_args = @{ + option_default = "1" + missing_option_default = $null + string_option = "1" + required_option = "required" + missing_choices = $null + choices = "a" + one_choice = "b" + choice_with_default = "b" + alias_direct = "a" + alias_as_alias = "a" + alias_as_alias2 = "a" + bool_type = $true + bool_from_str = $false + dict_type = @{ + int_type = 10 + str_type = "str_sub_type" + } + dict_type_missing = $null + dict_type_defaults = @{ + int_type = $null + str_type = "str_sub_type" + } + dict_type_json = @{ + a = "a" + b = 1 + c = @("a", "b") + } + dict_type_str = @{ + a = "a" + b = "b 2" + c = "c" + } + float_type = 3.14159 + int_type = 0 + json_type = $m.Params.json_type.ToString() + json_type_dict = $m.Params.json_type_dict.ToString() + list_type = @("a", "b", 1, 2) + list_type_str = @("a", "b", "1", "2") + list_with_int = @(1, 2) + list_type_single = @("single") + list_with_dict = @( + @{ + int_type = 2 + str_type = "dict entry" + }, + @{ + int_type = 1 + str_type = "str_sub_type" + }, + @{ + int_type = $null + str_type = "str_sub_type" + } + ) + path_type = "$($env:SystemRoot)\System32" + path_type_nt = "\\?\%SystemRoot%\System32" + path_type_missing = "T:\missing\path" + raw_type_str = "str" + raw_type_int = 1 + sid_type = "S-1-5-18" + sid_from_name = "S-1-5-18" + str_type = "str" + delegate_type = 1234 + } + $actual.Keys.Count | Assert-Equals -Expected 2 + $actual.changed | Assert-Equals -Expected $false + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $expected_module_args} + } + + "No log values" = { + $spec = @{ + options = @{ + username = @{type = "str"} + password = @{type = "str"; no_log = $true} + password2 = @{type = "int"; no_log = $true} + dict = @{type = "dict"} + } + } + $complex_args = @{ + _ansible_module_name = "test_no_log" + username = "user - pass - name" + password = "pass" + password2 = 1234 + dict = @{ + data = "Oops this is secret: pass" + dict = @{ + pass = "plain" + hide = "pass" + sub_hide = "password" + int_hide = 123456 + } + list = @( + "pass", + "password", + 1234567, + "pa ss", + @{ + pass = "plain" + hide = "pass" + sub_hide = "password" + int_hide = 123456 + } + ) + custom = "pass" + } + } + + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + $m.Result.data = $complex_args.dict + + # verify params internally aren't masked + $m.Params.username | Assert-Equals -Expected "user - pass - name" + $m.Params.password | Assert-Equals -Expected "pass" + $m.Params.password2 | Assert-Equals -Expected 1234 + $m.Params.dict.custom | Assert-Equals -Expected "pass" + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + #$_test_out + + # verify no_log params are masked in invocation + $expected = @{ + invocation = @{ + module_args = @{ + password2 = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" + dict = @{ + dict = @{ + pass = "plain" + hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" + sub_hide = "****word" + int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" + } + custom = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" + list = @( + "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", + "****word", + "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", + "pa ss", + @{ + pass = "plain" + hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" + sub_hide = "****word" + int_hide = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" + } + ) + data = "Oops this is secret: ****" + } + username = "user - **** - name" + password = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER" + } + } + changed = $false + data = $complex_args.dict + } + $actual | Assert-DictionaryEquals -Expected $expected + + $expected_event = @' +test_no_log - Invoked with: + username: user - **** - name + dict: dict: sub_hide: ****word + pass: plain + int_hide: ****56 + hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER + data: Oops this is secret: **** + custom: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER + list: + - VALUE_SPECIFIED_IN_NO_LOG_PARAMETER + - ****word + - ****567 + - pa ss + - sub_hide: ****word + pass: plain + int_hide: ****56 + hide: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER + password2: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER + password: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER +'@ + $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message + $actual_event | Assert-DictionaryEquals -Expected $expected_event + } + + "Removed in version" = { + $spec = @{ + options = @{ + removed1 = @{removed_in_version = "2.1"} + removed2 = @{removed_in_version = "2.2"} + } + } + $complex_args = @{ + removed1 = "value" + } + + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{ + removed1 = "value" + removed2 = $null + } + } + deprecations = @( + @{ + message = "Param 'removed1' is deprecated. See the module docs for more information" + version = "2.1" + } + ) + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "Debug without debug set" = { + $complex_args = @{ + _ansible_debug = $false + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + $m.Debug("debug message") + $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message + $actual_event | Assert-Equals -Expected "undefined win module - Invoked with:`r`n " + } + + "Debug with debug set" = { + $complex_args = @{ + _ansible_debug = $true + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + $m.Debug("debug message") + $actual_event = (Get-EventLog -LogName Application -Source Ansible -Newest 1).Message + $actual_event | Assert-Equals -Expected "undefined win module - [DEBUG] debug message" + } + + "Deprecate and warn" = { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + $m.Deprecate("message", "2.8") + $actual_deprecate_event = Get-EventLog -LogName Application -Source Ansible -Newest 1 + $m.Warn("warning") + $actual_warn_event = Get-EventLog -LogName Application -Source Ansible -Newest 1 + + $actual_deprecate_event.Message | Assert-Equals -Expected "undefined win module - [DEPRECATION WARNING] message 2.8" + $actual_warn_event.EntryType | Assert-Equals -Expected "Warning" + $actual_warn_event.Message | Assert-Equals -Expected "undefined win module - [WARNING] warning" + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{} + } + warnings = @("warning") + deprecations = @(@{message = "message"; version = "2.8"}) + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "FailJson with message" = { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + + $failed = $false + try { + $m.FailJson("fail message") + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $failed + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{} + } + failed = $true + msg = "fail message" + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "FailJson with Exception" = { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + + try { + [System.IO.Path]::GetFullPath($null) + } catch { + $excp = $_.Exception + } + + $failed = $false + try { + $m.FailJson("fail message", $excp) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $failed + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{} + } + failed = $true + msg = "fail message" + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "FailJson with ErrorRecord" = { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + + try { + Get-Item -Path $null + } catch { + $error_record = $_ + } + + $failed = $false + try { + $m.FailJson("fail message", $error_record) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $failed + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{} + } + failed = $true + msg = "fail message" + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "FailJson with Exception and verbosity 3" = { + $complex_args = @{ + _ansible_verbosity = 3 + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + + try { + [System.IO.Path]::GetFullPath($null) + } catch { + $excp = $_.Exception + } + + $failed = $false + try { + $m.FailJson("fail message", $excp) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $failed + + $actual.changed | Assert-Equals -Expected $false + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{}} + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected "fail message" + $actual.exception.Contains('System.Management.Automation.MethodInvocationException: Exception calling "GetFullPath" with "1" argument(s)') | Assert-Equals -Expected $true + } + + "FailJson with ErrorRecord and verbosity 3" = { + $complex_args = @{ + _ansible_verbosity = 3 + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + + try { + Get-Item -Path $null + } catch { + $error_record = $_ + } + + $failed = $false + try { + $m.FailJson("fail message", $error_record) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $failed + + $actual.changed | Assert-Equals -Expected $false + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{}} + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected "fail message" + $actual.exception.Contains("Cannot bind argument to parameter 'Path' because it is null") | Assert-Equals -Expected $true + $actual.exception.Contains("+ Get-Item -Path `$null") | Assert-Equals -Expected $true + $actual.exception.Contains("ScriptStackTrace:") | Assert-Equals -Expected $true + } + + "Diff entry without diff set" = { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + $m.Diff.before = @{a = "a"} + $m.Diff.after = @{b = "b"} + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $failed + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{} + } + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "Diff entry with diff set" = { + $complex_args = @{ + _ansible_diff = $true + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + $m.Diff.before = @{a = "a"} + $m.Diff.after = @{b = "b"} + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $failed + + $expected = @{ + changed = $false + invocation = @{ + module_args = @{} + } + diff = @{ + before = @{a = "a"} + after = @{b = "b"} + } + } + $actual | Assert-DictionaryEquals -Expected $expected + } + + "ParseBool tests" = { + $mapping = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[Object], [Bool]]' + $mapping.Add("y", $true) + $mapping.Add("Y", $true) + $mapping.Add("yes", $true) + $mapping.Add("Yes", $true) + $mapping.Add("on", $true) + $mapping.Add("On", $true) + $mapping.Add("1", $true) + $mapping.Add(1, $true) + $mapping.Add("true", $true) + $mapping.Add("True", $true) + $mapping.Add("t", $true) + $mapping.Add("T", $true) + $mapping.Add("1.0", $true) + $mapping.Add(1.0, $true) + $mapping.Add($true, $true) + $mapping.Add("n", $false) + $mapping.Add("N", $false) + $mapping.Add("no", $false) + $mapping.Add("No", $false) + $mapping.Add("off", $false) + $mapping.Add("Off", $false) + $mapping.Add("0", $false) + $mapping.Add(0, $false) + $mapping.Add("false", $false) + $mapping.Add("False", $false) + $mapping.Add("f", $false) + $mapping.Add("F", $false) + $mapping.Add("0.0", $false) + $mapping.Add(0.0, $false) + $mapping.Add($false, $false) + + foreach ($map in $mapping.GetEnumerator()) { + $expected = $map.Value + $actual = [Ansible.Basic.AnsibleModule]::ParseBool($map.Key) + $actual | Assert-Equals -Expected $expected + $actual.GetType().FullName | Assert-Equals -Expected "System.Boolean" + } + + $fail_bools = @( + "falsey", + "abc", + 2, + "2", + -1 + ) + foreach ($fail_bool in $fail_bools) { + $failed = $false + try { + [Ansible.Basic.AnsibleModule]::ParseBool($fail_bool) + } catch { + $failed = $true + $_.Exception.Message.Contains("The value '$fail_bool' is not a valid boolean") | Assert-Equals -Expected $true + } + $failed | Assert-Equals -Expected $true + } + } + + "Unknown internal key" = { + $complex_args = @{ + _ansible_invalid = "invalid" + } + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + + $expected = @{ + invocation = @{ + module_args = @{ + _ansible_invalid = "invalid" + } + } + changed = $false + failed = $true + msg = "Unsupported parameters for (undefined win module) module: _ansible_invalid. Supported parameters include: " + } + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + $actual | Assert-DictionaryEquals -Expected $expected + } + $failed | Assert-Equals -Expected $true + } + + "Module tmpdir with present remote tmp" = { + $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity + $dir_security.SetOwner($current_user) + $dir_security.SetAccessRuleProtection($true, $false) + $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @( + $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl, + [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit", + [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow + ) + $dir_security.AddAccessRule($ace) + $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner") + + $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)" + New-Item -Path $remote_tmp -ItemType Directory > $null + $complex_args = @{ + _ansible_remote_tmp = $remote_tmp.ToString() + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $true + + $actual_tmpdir = $m.Tmpdir + $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent + $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf + + $parent_tmpdir | Assert-Equals -Expected $remote_tmp + $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equals -Expected $true + (Test-Path -Path $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $true + $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp) + $children.Count | Assert-Equals -Expected 1 + $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner") + $actual_tmpdir_sd | Assert-Equals -Expected $expected_sd + + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $output = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + (Test-Path -Path $actual_tmpdir -PathType Container) | Assert-Equals -Expected $false + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $true + $output.warnings.Count | Assert-Equals -Expected 0 + } + + "Module tmpdir with missing remote_tmp" = { + $current_user = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + $dir_security = New-Object -TypeName System.Security.AccessControl.DirectorySecurity + $dir_security.SetOwner($current_user) + $dir_security.SetAccessRuleProtection($true, $false) + $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @( + $current_user, [System.Security.AccessControl.FileSystemRights]::FullControl, + [System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit", + [System.Security.AccessControl.PropagationFlags]::None, [System.Security.AccessControl.AccessControlType]::Allow + ) + $dir_security.AddAccessRule($ace) + $expected_sd = $dir_security.GetSecurityDescriptorSddlForm("Access, Owner") + + $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)" + $complex_args = @{ + _ansible_remote_tmp = $remote_tmp.ToString() + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $false + + $actual_tmpdir = $m.Tmpdir + $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent + $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf + + $parent_tmpdir | Assert-Equals -Expected $remote_tmp + $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equals -Expected $true + (Test-Path -Path $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $true + $children = [System.IO.Directory]::EnumerateDirectories($remote_tmp) + $children.Count | Assert-Equals -Expected 1 + $actual_remote_sd = (Get-Acl -Path $remote_tmp).GetSecurityDescriptorSddlForm("Access, Owner") + $actual_tmpdir_sd = (Get-Acl -Path $actual_tmpdir).GetSecurityDescriptorSddlForm("Access, Owner") + $actual_remote_sd | Assert-Equals -Expected $expected_sd + $actual_tmpdir_sd | Assert-Equals -Expected $expected_sd + + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $output = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + (Test-Path -Path $actual_tmpdir -PathType Container) | Assert-Equals -Expected $false + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $true + $output.warnings.Count | Assert-Equals -Expected 1 + $nt_account = $current_user.Translate([System.Security.Principal.NTAccount]) + $actual_warning = "Module remote_tmp $remote_tmp did not exist and was created with FullControl to $nt_account, " + $actual_warning += "this may cause issues when running as another user. To avoid this, " + $actual_warning += "create the remote_tmp dir with the correct permissions manually" + $actual_warning | Assert-Equals -Expected $output.warnings[0] + } + + "Module tmp, keep remote files" = { + $remote_tmp = Join-Path -Path $tmpdir -ChildPath "moduletmpdir-$(Get-Random)" + New-Item -Path $remote_tmp -ItemType Directory > $null + $complex_args = @{ + _ansible_remote_tmp = $remote_tmp.ToString() + _ansible_keep_remote_files = $true + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), @{}) + + $actual_tmpdir = $m.Tmpdir + $parent_tmpdir = Split-Path -Path $actual_tmpdir -Parent + $tmpdir_name = Split-Path -Path $actual_tmpdir -Leaf + + $parent_tmpdir | Assert-Equals -Expected $remote_tmp + $tmpdir_name.StartSwith("ansible-moduletmp-") | Assert-Equals -Expected $true + (Test-Path -Path $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $true + + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $output = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + (Test-Path -Path $actual_tmpdir -PathType Container) | Assert-Equals -Expected $true + (Test-Path -Path $remote_tmp -PathType Container) | Assert-Equals -Expected $true + $output.warnings.Count | Assert-Equals -Expected 0 + Remove-Item -Path $actual_tmpdir -Force -Recurse + } + + "Invalid argument spec key" = { + $spec = @{ + invalid = $true + } + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, " + $expected_msg += "aliases, choices, default, elements, mutually_exclusive, no_log, options, removed_in_version, " + $expected_msg += "required, required_if, required_one_of, required_together, supports_check_mode, type" + + $actual.Keys.Count | Assert-Equals -Expected 3 + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true + } + + "Invalid argument spec key - nested" = { + $spec = @{ + options = @{ + option_key = @{ + options = @{ + sub_option_key = @{ + invalid = $true + } + } + } + } + } + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "internal error: argument spec entry contains an invalid key 'invalid', valid keys: apply_defaults, " + $expected_msg += "aliases, choices, default, elements, mutually_exclusive, no_log, options, removed_in_version, " + $expected_msg += "required, required_if, required_one_of, required_together, supports_check_mode, type - " + $expected_msg += "found in option_key -> sub_option_key." + + $actual.Keys.Count | Assert-Equals -Expected 3 + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true + } + + "Invalid argument spec value type" = { + $spec = @{ + apply_defaults = "abc" + } + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "internal error: argument spec for 'apply_defaults' did not match expected " + $expected_msg += "type System.Boolean: actual type System.String" + + $actual.Keys.Count | Assert-Equals -Expected 3 + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true + } + + "Invalid argument spec option type" = { + $spec = @{ + options = @{ + option_key = @{ + type = "invalid type" + } + } + } + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "internal error: type 'invalid type' is unsupported - found in option_key. " + $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str" + + $actual.Keys.Count | Assert-Equals -Expected 3 + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true + } + + "Invalid argument spec option element type" = { + $spec = @{ + options = @{ + option_key = @{ + type = "list" + elements = "invalid type" + } + } + } + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "internal error: elements 'invalid type' is unsupported - found in option_key. " + $expected_msg += "Valid types are: bool, dict, float, int, json, list, path, raw, sid, str" + + $actual.Keys.Count | Assert-Equals -Expected 3 + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true + } + + "Spec required and default set at the same time" = { + $spec = @{ + options = @{ + option_key = @{ + required = $true + default = "default value" + } + } + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "internal error: required and default are mutually exclusive for option_key" + + $actual.Keys.Count | Assert-Equals -Expected 3 + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + ("exception" -cin $actual.Keys) | Assert-Equals -Expected $true + } + + "Unsupported options" = { + $spec = @{ + options = @{ + option_key = @{ + type = "str" + } + } + } + $complex_args = @{ + option_key = "abc" + invalid_key = "def" + another_key = "ghi" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "Unsupported parameters for (undefined win module) module: another_key, invalid_key. " + $expected_msg += "Supported parameters include: option_key" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Check mode and module doesn't support check mode" = { + $spec = @{ + options = @{ + option_key = @{ + type = "str" + } + } + } + $complex_args = @{ + _ansible_check_mode = $true + option_key = "abc" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "remote module (undefined win module) does not support check mode" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.skipped | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{option_key = "abc"}} + } + + "Type conversion error" = { + $spec = @{ + options = @{ + option_key = @{ + type = "int" + } + } + } + $complex_args = @{ + option_key = "a" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "argument for option_key is of type System.String and we were unable to convert to int: " + $expected_msg += "Input string was not in a correct format." + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Type conversion error - delegate" = { + $spec = @{ + options = @{ + option_key = @{ + type = "dict" + options = @{ + sub_option_key = @{ + type = [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0]) } + } + } + } + } + } + $complex_args = @{ + option_key = @{ + sub_option_key = "a" + } + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "argument for sub_option_key is of type System.String and we were unable to convert to delegate: " + $expected_msg += "Exception calling `"Parse`" with `"1`" argument(s): `"Input string was not in a correct format.`" " + $expected_msg += "found in option_key" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Invalid choice" = { + $spec = @{ + options = @{ + option_key = @{ + choices = "a", "b" + } + } + } + $complex_args = @{ + option_key = "c" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "value of option_key must be one of: a, b, got: c" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Invalid choice with no_log" = { + $spec = @{ + options = @{ + option_key = @{ + choices = "a", "b" + no_log = $true + } + } + } + $complex_args = @{ + option_key = "abc" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "value of option_key must be one of: a, b, got: ***" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = @{option_key = "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER"}} + } + + "Invalid choice in list" = { + $spec = @{ + options = @{ + option_key = @{ + choices = "a", "b" + type = "list" + } + } + } + $complex_args = @{ + option_key = "a", "c" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "value of option_key must be one or more of: a, b. Got no match for: c" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Mutually exclusive options" = { + $spec = @{ + options = @{ + option1 = @{} + option2 = @{} + } + mutually_exclusive = @(,@("option1", "option2")) + } + $complex_args = @{ + option1 = "a" + option2 = "b" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "parameters are mutually exclusive: option1, option2" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Missing required argument" = { + $spec = @{ + options = @{ + option1 = @{} + option2 = @{required = $true} + } + } + $complex_args = @{ + option1 = "a" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "missing required arguments: option2" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Missing required argument subspec - no value defined" = { + $spec = @{ + options = @{ + option_key = @{ + type = "dict" + options = @{ + sub_option_key = @{ + required = $true + } + } + } + } + } + + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $actual.Keys.Count | Assert-Equals -Expected 2 + $actual.changed | Assert-Equals -Expected $false + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Missing required argument subspec" = { + $spec = @{ + options = @{ + option_key = @{ + type = "dict" + options = @{ + sub_option_key = @{ + required = $true + } + another_key = @{} + } + } + } + } + $complex_args = @{ + option_key = @{ + another_key = "abc" + } + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "missing required arguments: sub_option_key found in option_key" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required together not set" = { + $spec = @{ + options = @{ + option1 = @{} + option2 = @{} + } + required_together = @(,@("option1", "option2")) + } + $complex_args = @{ + option1 = "abc" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "parameters are required together: option1, option2" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required together not set - subspec" = { + $spec = @{ + options = @{ + option_key = @{ + type = "dict" + options = @{ + option1 = @{} + option2 = @{} + } + required_together = @(,@("option1", "option2")) + } + another_option = @{} + } + required_together = @(,@("option_key", "another_option")) + } + $complex_args = @{ + option_key = @{ + option1 = "abc" + } + another_option = "def" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "parameters are required together: option1, option2 found in option_key" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required one of not set" = { + $spec = @{ + options = @{ + option1 = @{} + option2 = @{} + option3 = @{} + } + required_one_of = @(@("option1", "option2"), @("option2", "option3")) + } + $complex_args = @{ + option1 = "abc" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "one of the following is required: option2, option3" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required if invalid entries" = { + $spec = @{ + options = @{ + state = @{choices = "absent", "present"; default = "present"} + path = @{type = "path"} + } + required_if = @(,@("state", "absent")) + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "internal error: invalid required_if value count of 2, expecting 3 or 4 entries" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required if no missing option" = { + $spec = @{ + options = @{ + state = @{choices = "absent", "present"; default = "present"} + name = @{} + path = @{type = "path"} + } + required_if = @(,@("state", "absent", @("name", "path"))) + } + $complex_args = @{ + name = "abc" + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $actual.Keys.Count | Assert-Equals -Expected 2 + $actual.changed | Assert-Equals -Expected $false + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required if missing option" = { + $spec = @{ + options = @{ + state = @{choices = "absent", "present"; default = "present"} + name = @{} + path = @{type = "path"} + } + required_if = @(,@("state", "absent", @("name", "path"))) + } + $complex_args = @{ + state = "absent" + name = "abc" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "state is absent but all of the following are missing: path" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required if missing option and required one is set" = { + $spec = @{ + options = @{ + state = @{choices = "absent", "present"; default = "present"} + name = @{} + path = @{type = "path"} + } + required_if = @(,@("state", "absent", @("name", "path"), $true)) + } + $complex_args = @{ + state = "absent" + } + + $failed = $false + try { + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 1" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $expected_msg = "state is absent but any of the following are missing: name, path" + + $actual.Keys.Count | Assert-Equals -Expected 4 + $actual.changed | Assert-Equals -Expected $false + $actual.failed | Assert-Equals -Expected $true + $actual.msg | Assert-Equals -Expected $expected_msg + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } + + "Required if missing option but one required set" = { + $spec = @{ + options = @{ + state = @{choices = "absent", "present"; default = "present"} + name = @{} + path = @{type = "path"} + } + required_if = @(,@("state", "absent", @("name", "path"), $true)) + } + $complex_args = @{ + state = "absent" + name = "abc" + } + $m = [Ansible.Basic.AnsibleModule]::Create(@(), $spec) + + $failed = $false + try { + $m.ExitJson() + } catch [System.Management.Automation.RuntimeException] { + $failed = $true + $_.Exception.Message | Assert-Equals -Expected "exit: 0" + $actual = [Ansible.Basic.AnsibleModule]::FromJson($_test_out) + } + $failed | Assert-Equals -Expected $true + + $actual.Keys.Count | Assert-Equals -Expected 2 + $actual.changed | Assert-Equals -Expected $false + $actual.invocation | Assert-DictionaryEquals -Expected @{module_args = $complex_args} + } +} + +try { + foreach ($test_impl in $tests.GetEnumerator()) { + # Reset the variables before each test + $complex_args = @{} + $_test_out = $null + + $test = $test_impl.Key + &$test_impl.Value + } + $module.Result.data = "success" +} catch [System.Management.Automation.RuntimeException] { + $module.Result.failed = $true + $module.Result.test = $test + $module.Result.line = $_.InvocationInfo.ScriptLineNumber + $module.Result.method = $_.InvocationInfo.Line.Trim() + + if ($_.Exception.Message.StartSwith("exit: ")) { + # The exception was caused by an unexpected Exit call, log that on the output + $module.Result.output = (ConvertFrom-Json -InputObject $_test_out) + $module.Result.msg = "Uncaught AnsibleModule exit in tests, see output" + } else { + # Unrelated exception + $module.Result.exception = $_.Exception.ToString() + $module.Result.msg = "Uncaught exception: $(($_ | Out-String).ToString())" + } +} + +Exit-Module + diff --git a/test/integration/targets/win_csharp_utils/tasks/main.yml b/test/integration/targets/win_csharp_utils/tasks/main.yml new file mode 100644 index 00000000000..010c2d5076c --- /dev/null +++ b/test/integration/targets/win_csharp_utils/tasks/main.yml @@ -0,0 +1,9 @@ +--- +- name: test Ansible.Basic.cs + ansible_basic_tests: + register: ansible_basic_test + +- name: assert test Ansible.Basic.cs + assert: + that: + - ansible_basic_test.data == "success" diff --git a/test/integration/targets/win_environment/tasks/main.yml b/test/integration/targets/win_environment/tasks/main.yml index c779709948f..34f46f3bb3e 100644 --- a/test/integration/targets/win_environment/tasks/main.yml +++ b/test/integration/targets/win_environment/tasks/main.yml @@ -15,7 +15,7 @@ state: present level: machine register: create_fail_null - failed_when: create_fail_null.msg != "When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent" + failed_when: 'create_fail_null.msg != "state is present but all of the following are missing: value"' - name: fail to create environment value with empty value win_environment: @@ -24,7 +24,7 @@ state: present level: machine register: create_fail_empty_string - failed_when: create_fail_null.msg != "When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent" + failed_when: create_fail_empty_string.msg != "When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent" - name: create test environment value for machine check win_environment: diff --git a/test/integration/targets/win_ping/tasks/main.yml b/test/integration/targets/win_ping/tasks/main.yml index 4ed8cd9af7a..303702e1e4c 100644 --- a/test/integration/targets/win_ping/tasks/main.yml +++ b/test/integration/targets/win_ping/tasks/main.yml @@ -51,37 +51,6 @@ - win_ping_ps1_result is not changed - win_ping_ps1_result.ping == 'bleep' -# TODO: this will have to be removed once PS basic is implemented -- name: test win_ping with extra args to verify that v2 module replacer escaping works as expected - win_ping: - data: bloop - a_null: null - a_boolean: true - another_boolean: false - a_number: 299792458 - another_number: 22.7 - yet_another_number: 6.022e23 - a_string: | - it's magic - "@' - '@" - an_array: - - first - - 2 - - 3.0 - an_object: - - the_thing: the_value - - the_other_thing: 0 - - the_list_of_things: [1, 2, 3, 5] - register: win_ping_extra_args_result - -- name: check that win_ping with extra args succeeds and ignores everything except data - assert: - that: - - win_ping_extra_args_result is not failed - - win_ping_extra_args_result is not changed - - win_ping_extra_args_result.ping == 'bloop' - - name: test win_ping using data=crash so that it throws an exception win_ping: data: crash diff --git a/test/sanity/pslint/ignore.txt b/test/sanity/pslint/ignore.txt index ed85522a774..e5cbd33ced0 100644 --- a/test/sanity/pslint/ignore.txt +++ b/test/sanity/pslint/ignore.txt @@ -83,3 +83,4 @@ test/integration/targets/win_script/files/test_script.ps1 PSAvoidUsingWriteHost test/integration/targets/win_script/files/test_script_creates_file.ps1 PSAvoidUsingCmdletAliases test/integration/targets/win_script/files/test_script_with_args.ps1 PSAvoidUsingWriteHost test/integration/targets/win_script/files/test_script_with_splatting.ps1 PSAvoidUsingWriteHost +test/integration/targets/win_csharp_utils/library/ansible_basic_tests.ps1 PSUseDeclaredVarsMoreThanAssignments # test setup requires vars to be set globally and not referenced in the same scope diff --git a/test/sanity/validate-modules/main.py b/test/sanity/validate-modules/main.py index a0315124c4c..885b0a12a9b 100755 --- a/test/sanity/validate-modules/main.py +++ b/test/sanity/validate-modules/main.py @@ -702,6 +702,7 @@ class ModuleValidator(Validator): # check "shape" of each module name module_requires = r'(?im)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)' + csharp_requires = r'(?im)^#\s*ansiblerequires\s+\-csharputil\s*(Ansible\..+)' found_requires = False for req_stmt in re.finditer(module_requires, self.text): @@ -725,12 +726,33 @@ class ModuleValidator(Validator): msg='Module #Requires should not end in .psm1: "%s"' % module_name ) + for req_stmt in re.finditer(csharp_requires, self.text): + found_requires = True + # this will bomb on dictionary format - "don't do that" + module_list = [x.strip() for x in req_stmt.group(1).split(',')] + if len(module_list) > 1: + self.reporter.error( + path=self.object_path, + code=210, + msg='Ansible C# util requirements do not support multiple utils per statement: "%s"' % req_stmt.group(0) + ) + continue + + module_name = module_list[0] + + if module_name.lower().endswith('.cs'): + self.reporter.error( + path=self.object_path, + code=211, + msg='Module #AnsibleRequires -CSharpUtil should not end in .cs: "%s"' % module_name + ) + # also accept the legacy #POWERSHELL_COMMON replacer signal if not found_requires and REPLACER_WINDOWS not in self.text: self.reporter.error( path=self.object_path, code=207, - msg='No Ansible.ModuleUtils module requirements/imports found' + msg='No Ansible.ModuleUtils or C# Ansible util requirements/imports found' ) def _find_ps_docs_py_file(self):