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
This commit is contained in:
parent
74619c2036
commit
501acae5ab
17 changed files with 3644 additions and 314 deletions
2
changelogs/fragments/powershell_basic_util.yaml
Normal file
2
changelogs/fragments/powershell_basic_util.yaml
Normal file
|
@ -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.
|
|
@ -166,17 +166,18 @@ Windows new module development
|
||||||
When creating a new module there are a few things to keep in mind:
|
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
|
- 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
|
- Avoid using ``Write-Host/Debug/Verbose/Error`` in the module and add what needs to be returned to the ``$module.Result`` variable
|
||||||
- When trying an exception use ``Fail-Json -obj $result -message "exception message here"`` instead
|
- 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
|
- 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
|
- 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
|
- Try and catch specific exceptions when using try/catch statements
|
||||||
- Avoid using PSCustomObjects unless necessary
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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 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``
|
- 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 <https://github.com/ansible/ansi
|
||||||
|
|
||||||
A slightly more advanced module is `win_uri <https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/windows/win_uri.ps1>`_ 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.
|
A slightly more advanced module is `win_uri <https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/windows/win_uri.ps1>`_ 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 <https://docs.microsoft.com/en-us/dotnet/api/system.single?view=netframework-4.7.2>`_ 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=<type>`` 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 <https://docs.microsoft.com/en-us/dotnet/api/system.security.principal.securityidentifier?view=netframework-4.7.2>`_ 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
|
When in doubt, look at some of the other core modules and see how things have been
|
||||||
implemented there.
|
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.
|
- ``#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 -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 -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.<module_util>``: 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.<module_util>;`` to the top of the script with all the other
|
||||||
|
using statements.
|
||||||
|
|
||||||
|
|
||||||
Windows module utilities
|
Windows module utilities
|
||||||
|
@ -230,7 +292,56 @@ can be imported by adding the following line to a PowerShell module:
|
||||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
|
||||||
This will import the module_util at ``./lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1``
|
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 <assembly dll> [-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 <error id> [-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 <https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/index>`_
|
||||||
|
|
||||||
|
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
|
The following is a list of module_utils that are packaged with Ansible and a general description of what
|
||||||
they do:
|
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
|
folder called ``module_utils`` located in the root folder of the playbook or role
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
The below example is a role structure that contains two custom module_utils
|
C# module utilities can also be stored outside of the standard Ansible distribution for use with custom modules. Like
|
||||||
called ``Ansible.ModuleUtils.ModuleUtil1`` and
|
PowerShell utils, these are stored in a folder called ``module_utils`` and the filename must end in the extension
|
||||||
``Ansible.ModuleUtils.ModuleUtil2``::
|
``.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/
|
meta/
|
||||||
main.yml
|
main.yml
|
||||||
|
@ -262,15 +377,16 @@ called ``Ansible.ModuleUtils.ModuleUtil1`` and
|
||||||
module_utils/
|
module_utils/
|
||||||
Ansible.ModuleUtils.ModuleUtil1.psm1
|
Ansible.ModuleUtils.ModuleUtil1.psm1
|
||||||
Ansible.ModuleUtils.ModuleUtil2.psm1
|
Ansible.ModuleUtils.ModuleUtil2.psm1
|
||||||
|
Ansible.CustomUtil.cs
|
||||||
tasks/
|
tasks/
|
||||||
main.yml
|
main.yml
|
||||||
|
|
||||||
Each module_util must contain at least one function, and a list of functions, aliases and cmdlets to export for use
|
Each PowerShell module_util must contain at least one function that has been exported with ``Export-ModuleMember``
|
||||||
in a module. This can be a blanket export by using ``*``. For example:
|
at the end of the file. For example
|
||||||
|
|
||||||
.. code-block:: powershell
|
.. code-block:: powershell
|
||||||
|
|
||||||
Export-ModuleMember -Alias * -Function * -Cmdlet *
|
Export-ModuleMember -Function Invoke-CustomUtil, Get-CustomInfo
|
||||||
|
|
||||||
|
|
||||||
Windows playbook module testing
|
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:
|
are some steps that need to be followed to set this up:
|
||||||
|
|
||||||
- Copy the module script to the Windows server
|
- 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
|
- Copy the folders ``./lib/ansible/module_utils/powershell`` and ``./lib/ansible/module_utils/csharp`` 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::
|
- 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:
|
||||||
|
|
||||||
ConvertTo-Json -InputObject $obj -Depth 99
|
.. code-block:: powershell
|
||||||
|
|
||||||
- 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::
|
# Set $ErrorActionPreference to what's set during Ansible execution
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
Write-Error -Message (ConvertTo-Json -InputObject $obj -Depth 99)
|
# Set the first argument file to a JSON that contains the module args
|
||||||
|
$args = @("$($pwd.Path)\args.json")
|
||||||
|
|
||||||
- Add the following to the start of the module script that was copied to the server::
|
# Or instead of an args file, set $complex_args to the pre-processed module args
|
||||||
|
|
||||||
### start setup code
|
|
||||||
$complex_args = @{
|
$complex_args = @{
|
||||||
"_ansible_check_mode" = $false
|
_ansible_check_mode = $false
|
||||||
"_ansible_diff" = $false
|
_ansible_diff = $false
|
||||||
"path" = "C:\temp"
|
path = "C:\temp"
|
||||||
"state" = "present"
|
state = "present"
|
||||||
}
|
}
|
||||||
|
|
||||||
Import-Module -Name .\Ansible.ModuleUtils.Legacy.psm1
|
# Import any C# utils referenced with '#AnsibleRequires -CSharpUtil' or 'using Ansible.;
|
||||||
### end setup code
|
Import-Module -Name "$($pwd.Path)\powershell\Ansible.ModuleUtils.AddType.psm1"
|
||||||
|
$_csharp_utils = @(
|
||||||
|
"$($pwd.Path)\csharp\Ansible.Basic.cs"
|
||||||
|
)
|
||||||
|
Add-CSharpType -References $_csharp_utils -IncludeDebugInfo
|
||||||
|
|
||||||
You can add more args to ``$complex_args`` as required by the module. The
|
# Import any PowerShell modules referenced with '#Requires -Module`
|
||||||
module can now be run on the Windows host either directly through Powershell
|
Import-Module -Name "$($pwd.Path)\powershell\Ansible.ModuleUtils.Legacy.psm1"
|
||||||
or through an IDE.
|
|
||||||
|
# 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
|
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`_
|
- `Powershell ISE`_
|
||||||
- `Visual Studio Code`_
|
- `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.
|
- 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-``.
|
- Navigate to ``%TEMP%\..``. It should contain a folder starting with ``ansible-tmp-``.
|
||||||
- Inside this folder, open the PowerShell script for the module.
|
- 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
|
Windows unit testing
|
||||||
|
|
|
@ -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')
|
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) \
|
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'#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_style = 'new'
|
||||||
module_substyle = 'powershell'
|
module_substyle = 'powershell'
|
||||||
elif REPLACER_JSONARGS in b_module_data:
|
elif REPLACER_JSONARGS in b_module_data:
|
||||||
|
|
1242
lib/ansible/module_utils/csharp/Ansible.Basic.cs
Normal file
1242
lib/ansible/module_utils/csharp/Ansible.Basic.cs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -19,9 +19,8 @@ Function Add-CSharpType {
|
||||||
[Switch] Whether to return the loaded Assembly
|
[Switch] Whether to return the loaded Assembly
|
||||||
|
|
||||||
.PARAMETER AnsibleModule
|
.PARAMETER AnsibleModule
|
||||||
TODO - This is an AnsibleModule object that is used to derive the
|
[Ansible.Basic.AnsibleModule] used to derive the TempPath and Debug values.
|
||||||
TempPath and Debug values.
|
TempPath is set to the Tmpdir property of the class
|
||||||
TempPath is set to the TmpDir property of the class
|
|
||||||
IncludeDebugInfo is set when the Ansible verbosity is >= 3
|
IncludeDebugInfo is set when the Ansible verbosity is >= 3
|
||||||
|
|
||||||
.PARAMETER TempPath
|
.PARAMETER TempPath
|
||||||
|
@ -69,8 +68,8 @@ Function Add-CSharpType {
|
||||||
}
|
}
|
||||||
|
|
||||||
# pattern used to find referenced assemblies in the code
|
# pattern used to find referenced assemblies in the code
|
||||||
$assembly_pattern = "^//\s*AssemblyReference\s+-Name\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?$"
|
$assembly_pattern = [Regex]"//\s*AssemblyReference\s+-Name\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
|
||||||
$no_warn_pattern = "^//\s*NoWarn\s+-Name\s+(?<Name>[\w\d]*)(\s+-CLR\s+(?<CLR>Core|Framework))?$"
|
$no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?<Name>[\w\d]*)(\s+-CLR\s+(?<CLR>Core|Framework))?"
|
||||||
|
|
||||||
# PSCore vs PSDesktop use different methods to compile the code,
|
# PSCore vs PSDesktop use different methods to compile the code,
|
||||||
# PSCore uses Roslyn and can compile the code purely in memory
|
# PSCore uses Roslyn and can compile the code purely in memory
|
||||||
|
@ -99,29 +98,26 @@ Function Add-CSharpType {
|
||||||
foreach ($reference in $References) {
|
foreach ($reference in $References) {
|
||||||
# scan through code and add any assemblies that match
|
# scan through code and add any assemblies that match
|
||||||
# //AssemblyReference -Name ... [-CLR Core]
|
# //AssemblyReference -Name ... [-CLR Core]
|
||||||
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
|
# //NoWarn -Name ... [-CLR Core]
|
||||||
try {
|
$assembly_matches = $assembly_pattern.Matches($reference)
|
||||||
while ($null -ne ($line = $sr.ReadLine())) {
|
foreach ($match in $assembly_matches) {
|
||||||
if ($line -imatch $assembly_pattern) {
|
$clr = $match.Groups["CLR"].Value
|
||||||
# verify the reference is not for .NET Framework
|
if ($clr -and $clr -ne "Core") {
|
||||||
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$assembly_path = $Matches.Name
|
$assembly_path = $match.Groups["Name"]
|
||||||
if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) {
|
if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) {
|
||||||
$assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path
|
$assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path
|
||||||
}
|
}
|
||||||
$assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null
|
$assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null
|
||||||
}
|
}
|
||||||
if ($line -imatch $no_warn_pattern) {
|
$warn_matches = $no_warn_pattern.Matches($reference)
|
||||||
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") {
|
foreach ($match in $warn_matches) {
|
||||||
|
$clr = $match.Groups["CLR"].Value
|
||||||
|
if ($clr -and $clr -ne "Core") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$ignore_warnings.Add($Matches.Name, [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress)
|
$ignore_warnings.Add($match.Groups["Name"], [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress)
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
$sr.Close()
|
|
||||||
}
|
}
|
||||||
$syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null
|
$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
|
# configure compile options based on input
|
||||||
if ($PSCmdlet.ParameterSetName -eq "Module") {
|
if ($PSCmdlet.ParameterSetName -eq "Module") {
|
||||||
$temp_path = $AnsibleModule.TmpDir
|
$temp_path = $AnsibleModule.Tmpdir
|
||||||
$include_debug = $AnsibleModule.Verbosity -ge 3
|
$include_debug = $AnsibleModule.Verbosity -ge 3
|
||||||
} else {
|
} else {
|
||||||
$temp_path = $TempPath
|
$temp_path = $TempPath
|
||||||
|
@ -231,35 +227,33 @@ Function Add-CSharpType {
|
||||||
|
|
||||||
# create a code snippet for each reference and check if we need
|
# create a code snippet for each reference and check if we need
|
||||||
# to reference any extra assemblies
|
# to reference any extra assemblies
|
||||||
# //AssemblyReference -Name ... [-CLR Framework]
|
|
||||||
$ignore_warnings = [System.Collections.ArrayList]@()
|
$ignore_warnings = [System.Collections.ArrayList]@()
|
||||||
$compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@()
|
$compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@()
|
||||||
foreach ($reference in $References) {
|
foreach ($reference in $References) {
|
||||||
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
|
# scan through code and add any assemblies that match
|
||||||
try {
|
# //AssemblyReference -Name ... [-CLR Framework]
|
||||||
while ($null -ne ($line = $sr.ReadLine())) {
|
# //NoWarn -Name ... [-CLR Framework]
|
||||||
if ($line -imatch $assembly_pattern) {
|
$assembly_matches = $assembly_pattern.Matches($reference)
|
||||||
# verify the reference is not for .NET Core
|
foreach ($match in $assembly_matches) {
|
||||||
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") {
|
$clr = $match.Groups["CLR"].Value
|
||||||
|
if ($clr -and $clr -ne "Framework") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$assemblies.Add($Matches.Name) > $null
|
$assemblies.Add($match.Groups["Name"].Value) > $null
|
||||||
}
|
}
|
||||||
if ($line -imatch $no_warn_pattern) {
|
$warn_matches = $no_warn_pattern.Matches($reference)
|
||||||
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") {
|
foreach ($match in $warn_matches) {
|
||||||
|
$clr = $match.Groups["CLR"].Value
|
||||||
|
if ($clr -and $clr -ne "Framework") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
$warning_id = $Matches.Name
|
$warning_id = $match.Groups["Name"].Value
|
||||||
# /nowarn should only contain the numeric part
|
# /nowarn should only contain the numeric part
|
||||||
if ($warning_id.StartsWith("CS")) {
|
if ($warning_id.StartsWith("CS")) {
|
||||||
$warning_id = $warning_id.Substring(2)
|
$warning_id = $warning_id.Substring(2)
|
||||||
}
|
}
|
||||||
$ignore_warnings.Add($warning_id) > $null
|
$ignore_warnings.Add($warning_id) > $null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
$sr.Close()
|
|
||||||
}
|
|
||||||
$compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null
|
$compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null
|
||||||
}
|
}
|
||||||
if ($ignore_warnings.Count -gt 0) {
|
if ($ignore_warnings.Count -gt 0) {
|
||||||
|
@ -270,7 +264,7 @@ Function Add-CSharpType {
|
||||||
|
|
||||||
# compile the code together and check for errors
|
# compile the code together and check for errors
|
||||||
$provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider
|
$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) {
|
if ($compile.Errors.HasErrors) {
|
||||||
$msg = "Failed to compile C# code: "
|
$msg = "Failed to compile C# code: "
|
||||||
foreach ($e in $compile.Errors) {
|
foreach ($e in $compile.Errors) {
|
||||||
|
|
|
@ -3,35 +3,36 @@
|
||||||
# Copyright: (c) 2017, Ansible Project
|
# Copyright: (c) 2017, Ansible Project
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# 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()
|
$spec = @{
|
||||||
$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues()
|
options = @{
|
||||||
|
state = @{ type = "str"; default = "present"; choices = "absent", "exported", "present" }
|
||||||
$params = Parse-Args $args -supports_check_mode $true
|
path = @{ type = "path" }
|
||||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
thumbprint = @{ type = "str" }
|
||||||
|
store_name = @{ type = "str"; default = "My"; choices = $store_name_values }
|
||||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "exported", "present"
|
store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values }
|
||||||
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty ($state -eq "present" -or $state -eq "exported")
|
password = @{ type = "str"; no_log = $true }
|
||||||
$thumbprint = Get-AnsibleParam -obj $params -name "thumbprint" -type "str" -failifempty ($state -eq "exported")
|
key_exportable = @{ type = "bool"; default = $true }
|
||||||
$store_name = Get-AnsibleParam -obj $params -name "store_name" -type "str" -default "My" -validateset $store_name_values
|
key_storage = @{ type = "str"; default = "default"; choices = "default", "machine", "user" }
|
||||||
$store_location = Get-AnsibleParam -obj $params -name "store_location" -type "str" -default "LocalMachine" -validateset $store_location_values
|
file_type = @{ type = "str"; default = "der"; choices = "der", "pem", "pkcs12" }
|
||||||
$password = Get-AnsibleParam -obj $params -name "password" -type "str"
|
}
|
||||||
$key_exportable = Get-AnsibleParam -obj $params -name "key_exportable" -type "bool" -default $true
|
required_if = @(
|
||||||
$key_storage = Get-AnsibleParam -obj $params -name "key_storage" -type "str" -default "default" -validateset "default", "machine", "user"
|
@("state", "absent", @("path", "thumbprint"), $true),
|
||||||
$file_type = Get-AnsibleParam -obj $params -name "file_type" -type "str" -default "der" -validateset "der", "pem", "pkcs12"
|
@("state", "exported", @("path", "thumbprint")),
|
||||||
|
@("state", "present", @("path"))
|
||||||
$result = @{
|
)
|
||||||
changed = $false
|
supports_check_mode = $true
|
||||||
thumbprints = @()
|
|
||||||
}
|
}
|
||||||
|
$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
|
# parses a certificate file and returns X509Certificate2Collection
|
||||||
if (-not (Test-Path -Path $path -PathType Leaf)) {
|
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
|
# 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 {
|
try {
|
||||||
$certs.Import($path, $password, $store_flags)
|
$certs.Import($path, $password, $store_flags)
|
||||||
} catch {
|
} 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
|
return $certs
|
||||||
}
|
}
|
||||||
|
|
||||||
Function New-CertFile($cert, $path, $type, $password) {
|
Function New-CertFile($module, $cert, $path, $type, $password) {
|
||||||
$content_type = switch ($type) {
|
$content_type = switch ($type) {
|
||||||
"pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
"pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
||||||
"der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
"der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
||||||
|
@ -72,18 +73,18 @@ Function New-CertFile($cert, $path, $type, $password) {
|
||||||
$missing_key = $true
|
$missing_key = $true
|
||||||
}
|
}
|
||||||
if ($missing_key) {
|
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) {
|
if (Test-Path -Path $path) {
|
||||||
Remove-Item -Path $path -Force
|
Remove-Item -Path $path -Force
|
||||||
$result.changed = $true
|
$module.Result.changed = $true
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$cert_bytes = $cert.Export($content_type, $password)
|
$cert_bytes = $cert.Export($content_type, $password)
|
||||||
} catch {
|
} 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
|
# Need to manually handle a PEM file
|
||||||
|
@ -95,26 +96,26 @@ Function New-CertFile($cert, $path, $type, $password) {
|
||||||
$file_encoding = [System.Text.Encoding]::ASCII
|
$file_encoding = [System.Text.Encoding]::ASCII
|
||||||
$cert_bytes = $file_encoding.GetBytes($cert_content)
|
$cert_bytes = $file_encoding.GetBytes($cert_content)
|
||||||
} elseif ($type -eq "pkcs12") {
|
} elseif ($type -eq "pkcs12") {
|
||||||
$result.key_exported = $false
|
$module.Result.key_exported = $false
|
||||||
if ($cert.PrivateKey -ne $null) {
|
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 {
|
try {
|
||||||
[System.IO.File]::WriteAllBytes($path, $cert_bytes)
|
[System.IO.File]::WriteAllBytes($path, $cert_bytes)
|
||||||
} catch [System.ArgumentNullException] {
|
} 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] {
|
} 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] {
|
} 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 {
|
} 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) {
|
Function Get-CertFileType($path, $password) {
|
||||||
|
@ -147,17 +148,27 @@ Function Get-CertFileType($path, $password) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]::$store_name
|
$state = $module.Params.state
|
||||||
$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]::$store_location
|
$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
|
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
|
||||||
try {
|
try {
|
||||||
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
||||||
} catch [System.Security.Cryptography.CryptographicException] {
|
} 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] {
|
} 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 {
|
} 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
|
$store_certificates = $store.Certificates
|
||||||
|
|
||||||
|
@ -166,45 +177,43 @@ try {
|
||||||
$cert_thumbprints = @()
|
$cert_thumbprints = @()
|
||||||
|
|
||||||
if ($path -ne $null) {
|
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) {
|
foreach ($cert in $certs) {
|
||||||
$cert_thumbprints += $cert.Thumbprint
|
$cert_thumbprints += $cert.Thumbprint
|
||||||
}
|
}
|
||||||
} elseif ($thumbprint -ne $null) {
|
} elseif ($thumbprint -ne $null) {
|
||||||
$cert_thumbprints += $thumbprint
|
$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) {
|
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)
|
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert_thumbprint, $false)
|
||||||
if ($found_certs.Count -gt 0) {
|
if ($found_certs.Count -gt 0) {
|
||||||
foreach ($found_cert in $found_certs) {
|
foreach ($found_cert in $found_certs) {
|
||||||
try {
|
try {
|
||||||
if (-not $check_mode) {
|
if (-not $module.CheckMode) {
|
||||||
$store.Remove($found_cert)
|
$store.Remove($found_cert)
|
||||||
}
|
}
|
||||||
} catch [System.Security.SecurityException] {
|
} 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 {
|
} 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") {
|
} elseif ($state -eq "exported") {
|
||||||
# TODO: Add support for PKCS7 and exporting a cert chain
|
# TODO: Add support for PKCS7 and exporting a cert chain
|
||||||
$result.thumbprints += $thumbprint
|
$module.Result.thumbprints += $thumbprint
|
||||||
$export = $true
|
$export = $true
|
||||||
if (Test-Path -Path $path -PathType Container) {
|
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) {
|
} elseif (Test-Path -Path $path -PathType Leaf) {
|
||||||
$actual_cert_type = Get-CertFileType -path $path -password $password
|
$actual_cert_type = Get-CertFileType -path $path -password $password
|
||||||
if ($actual_cert_type -eq $file_type) {
|
if ($actual_cert_type -eq $file_type) {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
# failed to load the file so we set the thumbprint to something
|
# failed to load the file so we set the thumbprint to something
|
||||||
# that will fail validation
|
# that will fail validation
|
||||||
|
@ -220,27 +229,27 @@ try {
|
||||||
if ($export) {
|
if ($export) {
|
||||||
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
|
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
|
||||||
if ($found_certs.Count -ne 1) {
|
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 {
|
} 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) {
|
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)
|
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false)
|
||||||
if ($found_certs.Count -eq 0) {
|
if ($found_certs.Count -eq 0) {
|
||||||
try {
|
try {
|
||||||
if (-not $check_mode) {
|
if (-not $module.CheckMode) {
|
||||||
$store.Add($cert)
|
$store.Add($cert)
|
||||||
}
|
}
|
||||||
} catch [System.Security.Cryptography.CryptographicException] {
|
} 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 {
|
} 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()
|
$store.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
Exit-Json -obj $result
|
$module.ExitJson()
|
||||||
|
|
|
@ -3,67 +3,59 @@
|
||||||
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# 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"
|
$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
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
$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
|
|
||||||
|
|
||||||
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
|
$name = $module.Params.name
|
||||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present"
|
$level = $module.Params.level
|
||||||
$value = Get-AnsibleParam -obj $params -name "value" -type "str"
|
$state = $module.Params.state
|
||||||
$level = Get-AnsibleParam -obj $params -name "level" -type "str" -validateSet "machine","user","process" -failifempty $true
|
$value = $module.Params.value
|
||||||
|
|
||||||
$before_value = [Environment]::GetEnvironmentVariable($name, $level)
|
$before_value = [Environment]::GetEnvironmentVariable($name, $level)
|
||||||
|
$module.Result.before_value = $before_value
|
||||||
$result = @{
|
$module.Result.value = $value
|
||||||
before_value = $before_value
|
|
||||||
changed = $false
|
|
||||||
value = $value
|
|
||||||
}
|
|
||||||
|
|
||||||
# When removing environment, set value to $null if set
|
# When removing environment, set value to $null if set
|
||||||
if ($state -eq "absent" -and $value) {
|
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
|
$value = $null
|
||||||
} elseif ($state -eq "present" -and (-not $value)) {
|
} 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 ($state -eq "present" -and $before_value -ne $value) {
|
if ($state -eq "present" -and $before_value -ne $value) {
|
||||||
|
if (-not $module.CheckMode) {
|
||||||
if (-not $check_mode) {
|
|
||||||
[Environment]::SetEnvironmentVariable($name, $value, $level)
|
[Environment]::SetEnvironmentVariable($name, $value, $level)
|
||||||
}
|
}
|
||||||
$result.changed = $true
|
$module.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} elseif ($state -eq "absent" -and $before_value -ne $null) {
|
} elseif ($state -eq "absent" -and $before_value -ne $null) {
|
||||||
|
if (-not $module.CheckMode) {
|
||||||
if (-not $check_mode) {
|
|
||||||
[Environment]::SetEnvironmentVariable($name, $null, $level)
|
[Environment]::SetEnvironmentVariable($name, $null, $level)
|
||||||
}
|
}
|
||||||
$result.changed = $true
|
$module.Result.changed = $true
|
||||||
|
|
||||||
if ($diff_mode) {
|
|
||||||
$result.diff = @{
|
|
||||||
prepared = " [$level]`n-$name = $before_value`n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Exit-Json -obj $result
|
$module.ExitJson()
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,20 @@
|
||||||
|
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# 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"
|
$spec = @{
|
||||||
|
options = @{
|
||||||
$params = Parse-Args $args -supports_check_mode $true
|
data = @{ type = "str"; default = "pong" }
|
||||||
|
}
|
||||||
$data = Get-AnsibleParam -obj $params -name "data" -type "str" -default "pong"
|
supports_check_mode = $true
|
||||||
|
}
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||||
|
$data = $module.Params.data
|
||||||
|
|
||||||
if ($data -eq "crash") {
|
if ($data -eq "crash") {
|
||||||
throw "boom"
|
throw "boom"
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = @{
|
$module.Result.ping = $data
|
||||||
changed = $false
|
$module.ExitJson()
|
||||||
ping = $data
|
|
||||||
}
|
|
||||||
|
|
||||||
Exit-Json $result
|
|
||||||
|
|
|
@ -4,65 +4,80 @@
|
||||||
# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
|
# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
|
||||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
# 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.CamelConversion
|
||||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$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)
|
||||||
|
|
||||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
$url = $module.Params.url
|
||||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
$method = $module.Params.method
|
||||||
|
$content_type = $module.Params.content_type
|
||||||
$url = Get-AnsibleParam -obj $params -name "url" -type "str" -failifempty $true
|
$headers = $module.Params.headers
|
||||||
$method = Get-AnsibleParam -obj $params "method" -type "str" -default "GET" -validateset "CONNECT","DELETE","GET","HEAD","MERGE","OPTIONS","PATCH","POST","PUT","REFRESH","TRACE"
|
$body = $module.Params.body
|
||||||
$content_type = Get-AnsibleParam -obj $params -name "content_type" -type "str"
|
$dest = $module.Params.dest
|
||||||
$headers = Get-AnsibleParam -obj $params -name "headers"
|
$user = $module.Params.user
|
||||||
$body = Get-AnsibleParam -obj $params -name "body"
|
$password = $module.Params.password
|
||||||
$dest = Get-AnsibleParam -obj $params -name "dest" -type "path"
|
$force_basic_auth = $module.Params.force_basic_auth
|
||||||
|
$creates = $module.Params.creates
|
||||||
$user = Get-AnsibleParam -obj $params -name "user" -type "str"
|
$removes = $module.Params.removes
|
||||||
$password = Get-AnsibleParam -obj $params -name "password" -type "str"
|
$follow_redirects = $module.Params.follow_redirects
|
||||||
$force_basic_auth = Get-AnsibleParam -obj $params -name "force_basic_auth" -type "bool" -default $false
|
$maximum_redirection = $module.Params.maximum_redirection
|
||||||
|
$return_content = $module.Params.return_content
|
||||||
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
$status_code = $module.Params.status_code
|
||||||
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
$timeout = $module.Params.timeout
|
||||||
|
$validate_certs = $module.Params.validate_certs
|
||||||
$follow_redirects = Get-AnsibleParam -obj $params -name "follow_redirects" -type "str" -default "safe" -validateset "all","none","safe"
|
$client_cert = $module.Params.client_cert
|
||||||
$maximum_redirection = Get-AnsibleParam -obj $params -name "maximum_redirection" -type "int" -default 50
|
$client_cert_password = $module.Params.client_cert_password
|
||||||
$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"
|
|
||||||
|
|
||||||
$JSON_CANDIDATES = @('text', 'json', 'javascript')
|
$JSON_CANDIDATES = @('text', 'json', 'javascript')
|
||||||
|
|
||||||
$result = @{
|
$module.Result.elapsed = 0
|
||||||
changed = $false
|
$module.Result.url = $url
|
||||||
elapsed = 0
|
|
||||||
url = $url
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($creates -and (Test-AnsiblePath -Path $creates)) {
|
if ($creates -and (Test-AnsiblePath -Path $creates)) {
|
||||||
$result.skipped = $true
|
$module.Result.skipped = $true
|
||||||
Exit-Json -obj $result -message "The 'creates' file or directory ($creates) already exists."
|
$module.Result.msg = "The 'creates' file or directory ($creates) already exists."
|
||||||
|
$module.ExitJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($removes -and -not (Test-AnsiblePath -Path $removes)) {
|
if ($removes -and -not (Test-AnsiblePath -Path $removes)) {
|
||||||
$result.skipped = $true
|
$module.Result.skipped = $true
|
||||||
Exit-Json -obj $result -message "The 'removes' file or directory ($removes) does not exist."
|
$module.Result.msg = "The 'removes' file or directory ($removes) does not exist."
|
||||||
}
|
$module.ExitJson()
|
||||||
|
|
||||||
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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5)
|
# 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
|
$req_headers = New-Object -TypeName System.Net.WebHeaderCollection
|
||||||
foreach ($header in $headers.GetEnumerator()) {
|
foreach ($header in $headers.GetEnumerator()) {
|
||||||
# some headers need to be set on the property itself
|
# some headers need to be set on the property itself
|
||||||
switch ($header.Name) {
|
switch ($header.Key) {
|
||||||
Accept { $client.Accept = $header.Value }
|
Accept { $client.Accept = $header.Value }
|
||||||
Connection { $client.Connection = $header.Value }
|
Connection { $client.Connection = $header.Value }
|
||||||
Content-Length { $client.ContentLength = $header.Value }
|
Content-Length { $client.ContentLength = $header.Value }
|
||||||
|
@ -130,7 +145,7 @@ if ($headers) {
|
||||||
$client.TransferEncoding = $header.Value
|
$client.TransferEncoding = $header.Value
|
||||||
}
|
}
|
||||||
User-Agent { $client.UserAgent = $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)
|
$client.Headers.Add($req_headers)
|
||||||
|
@ -138,15 +153,15 @@ if ($headers) {
|
||||||
|
|
||||||
if ($client_cert) {
|
if ($client_cert) {
|
||||||
if (-not (Test-AnsiblePath -Path $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 {
|
try {
|
||||||
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection -ArgumentList $client_cert, $client_cert_password
|
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection -ArgumentList $client_cert, $client_cert_password
|
||||||
$client.ClientCertificates = $certs
|
$client.ClientCertificates = $certs
|
||||||
} catch [System.Security.Cryptography.CryptographicException] {
|
} 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 {
|
} 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
|
$client.Credentials = $credential
|
||||||
}
|
}
|
||||||
} elseif ($user -or $password) {
|
} 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) {
|
if ($null -ne $body) {
|
||||||
|
@ -187,7 +202,7 @@ $module_start = Get-Date
|
||||||
try {
|
try {
|
||||||
$response = $client.GetResponse()
|
$response = $client.GetResponse()
|
||||||
} catch [System.Net.WebException] {
|
} catch [System.Net.WebException] {
|
||||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||||
$response = $null
|
$response = $null
|
||||||
if ($_.Exception.PSObject.Properties.Name -match "Response") {
|
if ($_.Exception.PSObject.Properties.Name -match "Response") {
|
||||||
# was a non-successful response but we at least have a response and
|
# 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
|
# in the case a response (or empty response) was on the exception like in
|
||||||
# a timeout scenario, we should still fail
|
# a timeout scenario, we should still fail
|
||||||
if ($null -eq $response) {
|
if ($null -eq $response) {
|
||||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||||
Fail-Json -obj $result -message "WebException occurred when sending web request: $($_.Exception.Message)"
|
$module.FailJson("WebException occurred when sending web request: $($_.Exception.Message)", $_)
|
||||||
}
|
}
|
||||||
} catch [System.Net.ProtocolViolationException] {
|
} catch [System.Net.ProtocolViolationException] {
|
||||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||||
Fail-Json -obj $result -message "ProtocolViolationException when sending web request: $($_.Exception.Message)"
|
$module.FailJson("ProtocolViolationException when sending web request: $($_.Exception.Message)", $_)
|
||||||
} catch {
|
} catch {
|
||||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||||
Fail-Json -obj $result -message "Unhandled exception occured when sending web request. Exception: $($_.Exception.Message)"
|
$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) {
|
ForEach ($prop in $response.psobject.properties) {
|
||||||
$result_key = Convert-StringToSnakeCase -string $prop.Name
|
$result_key = Convert-StringToSnakeCase -string $prop.Name
|
||||||
|
@ -217,7 +232,7 @@ ForEach ($prop in $response.psobject.properties) {
|
||||||
if ($prop_value -is [System.DateTime]) {
|
if ($prop_value -is [System.DateTime]) {
|
||||||
$prop_value = $prop_value.ToString("o", [System.Globalization.CultureInfo]::InvariantCulture)
|
$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
|
# 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_value = $response.Headers[$header_key]
|
||||||
$header_key = $header_key.Replace("-", "") # replace - with _ for snake conversion
|
$header_key = $header_key.Replace("-", "") # replace - with _ for snake conversion
|
||||||
$header_key = Convert-StringToSnakeCase -string $header_key
|
$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
|
# 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) {
|
if ($return_content) {
|
||||||
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
|
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
|
||||||
$content_bytes = $memory_st.ToArray()
|
$content_bytes = $memory_st.ToArray()
|
||||||
$result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes)
|
$module.Result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes)
|
||||||
if ($result.ContainsKey("content_type") -and $result.content_type -Match ($JSON_CANDIDATES -join '|')) {
|
if ($module.Result.ContainsKey("content_type") -and $module.Result.content_type -Match ($JSON_CANDIDATES -join '|')) {
|
||||||
try {
|
try {
|
||||||
$result.json = ConvertFrom-Json -InputObject $result.content
|
$module.Result.json = ([Ansible.Basic.AnsibleModule]::FromJson($module.Result.content))
|
||||||
} catch [System.ArgumentException] {
|
} catch [System.ArgumentException] {
|
||||||
# Simply continue, since 'text' might be anything
|
# Simply continue, since 'text' might be anything
|
||||||
}
|
}
|
||||||
|
@ -266,8 +281,8 @@ if ($return_content -or $dest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$result.changed = $changed
|
$module.Result.changed = $changed
|
||||||
if ($changed -and (-not $check_mode)) {
|
if ($changed -and (-not $module.CheckMode)) {
|
||||||
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
|
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
|
||||||
$file_stream = [System.IO.File]::Create($dest)
|
$file_stream = [System.IO.File]::Create($dest)
|
||||||
try {
|
try {
|
||||||
|
@ -284,7 +299,7 @@ if ($return_content -or $dest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($status_code -notcontains $response.StatusCode) {
|
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()
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
path: '{{win_cert_dir}}\subj-cert.pem'
|
path: '{{win_cert_dir}}\subj-cert.pem'
|
||||||
store_location: FakeLocation
|
store_location: FakeLocation
|
||||||
register: fail_fake_location
|
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
|
- name: fail with invalid store name
|
||||||
win_certificate_store:
|
win_certificate_store:
|
||||||
|
@ -13,27 +13,27 @@
|
||||||
path: '{{win_cert_dir}}\subj-cert.pem'
|
path: '{{win_cert_dir}}\subj-cert.pem'
|
||||||
store_name: FakeName
|
store_name: FakeName
|
||||||
register: fail_fake_name
|
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
|
- name: fail when state=present and no path is set
|
||||||
win_certificate_store:
|
win_certificate_store:
|
||||||
state: present
|
state: present
|
||||||
register: fail_present_no_path
|
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
|
- name: fail when state=exported and no path is set
|
||||||
win_certificate_store:
|
win_certificate_store:
|
||||||
state: exported
|
state: exported
|
||||||
thumbprint: ABC
|
thumbprint: ABC
|
||||||
register: fail_export_no_path
|
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
|
- name: fail when state=exported and no thumbprint is set
|
||||||
win_certificate_store:
|
win_certificate_store:
|
||||||
state: exported
|
state: exported
|
||||||
path: '{{win_cert_dir}}'
|
path: '{{win_cert_dir}}'
|
||||||
register: fail_export_no_thumbprint
|
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
|
- name: fail to export thumbprint when path is a dir
|
||||||
win_certificate_store:
|
win_certificate_store:
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
win_certificate_store:
|
win_certificate_store:
|
||||||
state: absent
|
state: absent
|
||||||
register: fail_absent_no_path_or_thumbprint
|
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)
|
- name: import pem certificate (check)
|
||||||
win_certificate_store:
|
win_certificate_store:
|
||||||
|
|
2
test/integration/targets/win_csharp_utils/aliases
Normal file
2
test/integration/targets/win_csharp_utils/aliases
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
shippable/windows/group1
|
||||||
|
shippable/windows/smoketest
|
File diff suppressed because it is too large
Load diff
9
test/integration/targets/win_csharp_utils/tasks/main.yml
Normal file
9
test/integration/targets/win_csharp_utils/tasks/main.yml
Normal file
|
@ -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"
|
|
@ -15,7 +15,7 @@
|
||||||
state: present
|
state: present
|
||||||
level: machine
|
level: machine
|
||||||
register: create_fail_null
|
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
|
- name: fail to create environment value with empty value
|
||||||
win_environment:
|
win_environment:
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
state: present
|
state: present
|
||||||
level: machine
|
level: machine
|
||||||
register: create_fail_empty_string
|
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
|
- name: create test environment value for machine check
|
||||||
win_environment:
|
win_environment:
|
||||||
|
|
|
@ -51,37 +51,6 @@
|
||||||
- win_ping_ps1_result is not changed
|
- win_ping_ps1_result is not changed
|
||||||
- win_ping_ps1_result.ping == 'bleep'
|
- 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
|
- name: test win_ping using data=crash so that it throws an exception
|
||||||
win_ping:
|
win_ping:
|
||||||
data: crash
|
data: crash
|
||||||
|
|
|
@ -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_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_args.ps1 PSAvoidUsingWriteHost
|
||||||
test/integration/targets/win_script/files/test_script_with_splatting.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
|
||||||
|
|
|
@ -702,6 +702,7 @@ class ModuleValidator(Validator):
|
||||||
# check "shape" of each module name
|
# check "shape" of each module name
|
||||||
|
|
||||||
module_requires = r'(?im)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)'
|
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
|
found_requires = False
|
||||||
|
|
||||||
for req_stmt in re.finditer(module_requires, self.text):
|
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
|
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
|
# also accept the legacy #POWERSHELL_COMMON replacer signal
|
||||||
if not found_requires and REPLACER_WINDOWS not in self.text:
|
if not found_requires and REPLACER_WINDOWS not in self.text:
|
||||||
self.reporter.error(
|
self.reporter.error(
|
||||||
path=self.object_path,
|
path=self.object_path,
|
||||||
code=207,
|
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):
|
def _find_ps_docs_py_file(self):
|
||||||
|
|
Loading…
Reference in a new issue