diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
commit | 8a754e0858d922e955e71b253c139e071ecec432 (patch) | |
tree | 527d16e74bfd1840c85efd675fdecad056c54107 /lib/ansible/module_utils/powershell | |
parent | Initial commit. (diff) | |
download | ansible-core-upstream/2.14.3.tar.xz ansible-core-upstream/2.14.3.zip |
Adding upstream version 2.14.3.upstream/2.14.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
12 files changed, 2318 insertions, 0 deletions
diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 new file mode 100644 index 0000000..6dc2917 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 @@ -0,0 +1,398 @@ +# Copyright (c) 2018 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +Function Add-CSharpType { + <# + .SYNOPSIS + Compiles one or more C# scripts similar to Add-Type. This exposes + more configuration options that are useable within Ansible and it + also allows multiple C# sources to be compiled together. + + .PARAMETER References + [String[]] A collection of C# scripts to compile together. + + .PARAMETER IgnoreWarnings + [Switch] Whether to compile code that contains compiler warnings, by + default warnings will cause a compiler error. + + .PARAMETER PassThru + [Switch] Whether to return the loaded Assembly + + .PARAMETER AnsibleModule + [Ansible.Basic.AnsibleModule] used to derive the TempPath and Debug values. + TempPath is set to the Tmpdir property of the class + IncludeDebugInfo is set when the Ansible verbosity is >= 3 + + .PARAMETER TempPath + [String] The temporary directory in which the dynamic assembly is + compiled to. This file is deleted once compilation is complete. + Cannot be used when AnsibleModule is set. This is a no-op when + running on PSCore. + + .PARAMETER IncludeDebugInfo + [Switch] Whether to include debug information in the compiled + assembly. Cannot be used when AnsibleModule is set. This is a no-op + when running on PSCore. + + .PARAMETER CompileSymbols + [String[]] A list of symbols to be defined during compile time. These are + added to the existing symbols, 'CORECLR', 'WINDOWS', 'UNIX' that are set + conditionalls in this cmdlet. + + .NOTES + The following features were added to control the compiling options from the + code itself. + + * Predefined compiler SYMBOLS + + * CORECLR - Added when running on PowerShell Core. + * WINDOWS - Added when running on Windows. + * UNIX - Added when running on non-Windows. + * X86 - Added when running on a 32-bit process (Ansible 2.10+) + * AMD64 - Added when running on a 64-bit process (Ansible 2.10+) + + * Ignore compiler warnings inline with the following comment inline + + //NoWarn -Name <rule code> [-CLR Core|Framework] + + * Specify custom assembly references inline + + //AssemblyReference -Name Dll.Location.dll [-CLR Core|Framework] + + # Added in Ansible 2.10 + //AssemblyReference -Type System.Type.Name [-CLR Core|Framework] + + * Create automatic type accelerators to simplify long namespace names (Ansible 2.9+) + + //TypeAccelerator -Name <AcceleratorName> -TypeName <Name of compiled type> + #> + param( + [Parameter(Mandatory = $true)][AllowEmptyCollection()][String[]]$References, + [Switch]$IgnoreWarnings, + [Switch]$PassThru, + [Parameter(Mandatory = $true, ParameterSetName = "Module")][Object]$AnsibleModule, + [Parameter(ParameterSetName = "Manual")][String]$TempPath = $env:TMP, + [Parameter(ParameterSetName = "Manual")][Switch]$IncludeDebugInfo, + [String[]]$CompileSymbols = @() + ) + if ($null -eq $References -or $References.Length -eq 0) { + return + } + + # define special symbols CORECLR, WINDOWS, UNIX if required + # the Is* variables are defined on PSCore, if absent we assume an + # older version of PowerShell under .NET Framework and Windows + $defined_symbols = [System.Collections.ArrayList]$CompileSymbols + + if ([System.IntPtr]::Size -eq 4) { + $defined_symbols.Add('X86') > $null + } + else { + $defined_symbols.Add('AMD64') > $null + } + + $is_coreclr = Get-Variable -Name IsCoreCLR -ErrorAction SilentlyContinue + if ($null -ne $is_coreclr) { + if ($is_coreclr.Value) { + $defined_symbols.Add("CORECLR") > $null + } + } + $is_windows = Get-Variable -Name IsWindows -ErrorAction SilentlyContinue + if ($null -ne $is_windows) { + if ($is_windows.Value) { + $defined_symbols.Add("WINDOWS") > $null + } + else { + $defined_symbols.Add("UNIX") > $null + } + } + else { + $defined_symbols.Add("WINDOWS") > $null + } + + # Store any TypeAccelerators shortcuts the util wants us to set + $type_accelerators = [System.Collections.Generic.List`1[Hashtable]]@() + + # pattern used to find referenced assemblies in the code + $assembly_pattern = [Regex]"//\s*AssemblyReference\s+-(?<Parameter>(Name)|(Type))\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?" + $no_warn_pattern = [Regex]"//\s*NoWarn\s+-Name\s+(?<Name>[\w\d]*)(\s+-CLR\s+(?<CLR>Core|Framework))?" + $type_pattern = [Regex]"//\s*TypeAccelerator\s+-Name\s+(?<Name>[\w.]*)\s+-TypeName\s+(?<TypeName>[\w.]*)" + + # PSCore vs PSDesktop use different methods to compile the code, + # PSCore uses Roslyn and can compile the code purely in memory + # without touching the disk while PSDesktop uses CodeDom and csc.exe + # to compile the code. We branch out here and run each + # distribution's method to add our C# code. + if ($is_coreclr) { + # compile the code using Roslyn on PSCore + + # Include the default assemblies using the logic in Add-Type + # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs + $assemblies = [System.Collections.Generic.HashSet`1[Microsoft.CodeAnalysis.MetadataReference]]@( + [Microsoft.CodeAnalysis.CompilationReference]::CreateFromFile(([System.Reflection.Assembly]::GetAssembly([PSObject])).Location) + ) + $netcore_app_ref_folder = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName([PSObject].Assembly.Location), "ref") + $lib_assembly_location = [System.IO.Path]::GetDirectoryName([object].Assembly.Location) + foreach ($file in [System.IO.Directory]::EnumerateFiles($netcore_app_ref_folder, "*.dll", [System.IO.SearchOption]::TopDirectoryOnly)) { + $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($file)) > $null + } + + # loop through the references, parse as a SyntaxTree and get + # referenced assemblies + $ignore_warnings = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[String], [Microsoft.CodeAnalysis.ReportDiagnostic]]' + $parse_options = ([Microsoft.CodeAnalysis.CSharp.CSharpParseOptions]::Default).WithPreprocessorSymbols($defined_symbols) + $syntax_trees = [System.Collections.Generic.List`1[Microsoft.CodeAnalysis.SyntaxTree]]@() + foreach ($reference in $References) { + # scan through code and add any assemblies that match + # //AssemblyReference -Name ... [-CLR Core] + # //NoWarn -Name ... [-CLR Core] + # //TypeAccelerator -Name ... -TypeName ... + $assembly_matches = $assembly_pattern.Matches($reference) + foreach ($match in $assembly_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Core") { + continue + } + + $parameter_type = $match.Groups["Parameter"].Value + $assembly_path = $match.Groups["Name"].Value + if ($parameter_type -eq "Type") { + $assembly_path = ([Type]$assembly_path).Assembly.Location + } + else { + if (-not ([System.IO.Path]::IsPathRooted($assembly_path))) { + $assembly_path = Join-Path -Path $lib_assembly_location -ChildPath $assembly_path + } + } + $assemblies.Add([Microsoft.CodeAnalysis.MetadataReference]::CreateFromFile($assembly_path)) > $null + } + $warn_matches = $no_warn_pattern.Matches($reference) + foreach ($match in $warn_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Core") { + continue + } + $ignore_warnings.Add($match.Groups["Name"], [Microsoft.CodeAnalysis.ReportDiagnostic]::Suppress) + } + $syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null + + $type_matches = $type_pattern.Matches($reference) + foreach ($match in $type_matches) { + $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value }) + } + } + + # Release seems to contain the correct line numbers compared to + # debug,may need to keep a closer eye on this in the future + $compiler_options = (New-Object -TypeName Microsoft.CodeAnalysis.CSharp.CSharpCompilationOptions -ArgumentList @( + [Microsoft.CodeAnalysis.OutputKind]::DynamicallyLinkedLibrary + )).WithOptimizationLevel([Microsoft.CodeAnalysis.OptimizationLevel]::Release) + + # set warnings to error out if IgnoreWarnings is not set + if (-not $IgnoreWarnings.IsPresent) { + $compiler_options = $compiler_options.WithGeneralDiagnosticOption([Microsoft.CodeAnalysis.ReportDiagnostic]::Error) + $compiler_options = $compiler_options.WithSpecificDiagnosticOptions($ignore_warnings) + } + + # create compilation object + $compilation = [Microsoft.CodeAnalysis.CSharp.CSharpCompilation]::Create( + [System.Guid]::NewGuid().ToString(), + $syntax_trees, + $assemblies, + $compiler_options + ) + + # Load the compiled code and pdb info, we do this so we can + # include line number in a stracktrace + $code_ms = New-Object -TypeName System.IO.MemoryStream + $pdb_ms = New-Object -TypeName System.IO.MemoryStream + try { + $emit_result = $compilation.Emit($code_ms, $pdb_ms) + if (-not $emit_result.Success) { + $errors = [System.Collections.ArrayList]@() + + foreach ($e in $emit_result.Diagnostics) { + # builds the error msg, based on logic in Add-Type + # https://github.com/PowerShell/PowerShell/blob/master/src/Microsoft.PowerShell.Commands.Utility/commands/utility/AddType.cs#L1239 + if ($null -eq $e.Location.SourceTree) { + $errors.Add($e.ToString()) > $null + continue + } + + $cancel_token = New-Object -TypeName System.Threading.CancellationToken -ArgumentList $false + $text_lines = $e.Location.SourceTree.GetText($cancel_token).Lines + $line_span = $e.Location.GetLineSpan() + + $diagnostic_message = $e.ToString() + $error_line_string = $text_lines[$line_span.StartLinePosition.Line].ToString() + $error_position = $line_span.StartLinePosition.Character + + $sb = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($diagnostic_message.Length + $error_line_string.Length * 2 + 4) + $sb.AppendLine($diagnostic_message) + $sb.AppendLine($error_line_string) + + for ($i = 0; $i -lt $error_line_string.Length; $i++) { + if ([System.Char]::IsWhiteSpace($error_line_string[$i])) { + continue + } + $sb.Append($error_line_string, 0, $i) + $sb.Append(' ', [Math]::Max(0, $error_position - $i)) + $sb.Append("^") + break + } + + $errors.Add($sb.ToString()) > $null + } + + throw [InvalidOperationException]"Failed to compile C# code:`r`n$($errors -join "`r`n")" + } + + $code_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $pdb_ms.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $compiled_assembly = [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream($code_ms, $pdb_ms) + } + finally { + $code_ms.Close() + $pdb_ms.Close() + } + } + else { + # compile the code using CodeDom on PSDesktop + + # configure compile options based on input + if ($PSCmdlet.ParameterSetName -eq "Module") { + $temp_path = $AnsibleModule.Tmpdir + $include_debug = $AnsibleModule.Verbosity -ge 3 + } + else { + $temp_path = $TempPath + $include_debug = $IncludeDebugInfo.IsPresent + } + $compiler_options = [System.Collections.ArrayList]@("/optimize") + if ($defined_symbols.Count -gt 0) { + $compiler_options.Add("/define:" + ([String]::Join(";", $defined_symbols.ToArray()))) > $null + } + + $compile_parameters = New-Object -TypeName System.CodeDom.Compiler.CompilerParameters + $compile_parameters.GenerateExecutable = $false + $compile_parameters.GenerateInMemory = $true + $compile_parameters.TreatWarningsAsErrors = (-not $IgnoreWarnings.IsPresent) + $compile_parameters.IncludeDebugInformation = $include_debug + $compile_parameters.TempFiles = (New-Object -TypeName System.CodeDom.Compiler.TempFileCollection -ArgumentList $temp_path, $false) + + # Add-Type automatically references System.dll, System.Core.dll, + # and System.Management.Automation.dll which we replicate here + $assemblies = [System.Collections.Generic.HashSet`1[String]]@( + "System.dll", + "System.Core.dll", + ([System.Reflection.Assembly]::GetAssembly([PSObject])).Location + ) + + # create a code snippet for each reference and check if we need + # to reference any extra assemblies + $ignore_warnings = [System.Collections.ArrayList]@() + $compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@() + foreach ($reference in $References) { + # scan through code and add any assemblies that match + # //AssemblyReference -Name ... [-CLR Framework] + # //NoWarn -Name ... [-CLR Framework] + # //TypeAccelerator -Name ... -TypeName ... + $assembly_matches = $assembly_pattern.Matches($reference) + foreach ($match in $assembly_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Framework") { + continue + } + + $parameter_type = $match.Groups["Parameter"].Value + $assembly_path = $match.Groups["Name"].Value + if ($parameter_type -eq "Type") { + $assembly_path = ([Type]$assembly_path).Assembly.Location + } + $assemblies.Add($assembly_path) > $null + } + $warn_matches = $no_warn_pattern.Matches($reference) + foreach ($match in $warn_matches) { + $clr = $match.Groups["CLR"].Value + if ($clr -and $clr -ne "Framework") { + continue + } + $warning_id = $match.Groups["Name"].Value + # /nowarn should only contain the numeric part + if ($warning_id.StartsWith("CS")) { + $warning_id = $warning_id.Substring(2) + } + $ignore_warnings.Add($warning_id) > $null + } + $compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null + + $type_matches = $type_pattern.Matches($reference) + foreach ($match in $type_matches) { + $type_accelerators.Add(@{Name = $match.Groups["Name"].Value; TypeName = $match.Groups["TypeName"].Value }) + } + } + if ($ignore_warnings.Count -gt 0) { + $compiler_options.Add("/nowarn:" + ([String]::Join(",", $ignore_warnings.ToArray()))) > $null + } + $compile_parameters.ReferencedAssemblies.AddRange($assemblies) + $compile_parameters.CompilerOptions = [String]::Join(" ", $compiler_options.ToArray()) + + # compile the code together and check for errors + $provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider + + # This calls csc.exe which can take compiler options from environment variables. Currently these env vars + # are known to have problems so they are unset: + # LIB - additional library paths will fail the compilation if they are invalid + $originalEnv = @{} + try { + 'LIB' | ForEach-Object -Process { + $value = Get-Item -LiteralPath "Env:\$_" -ErrorAction SilentlyContinue + if ($value) { + $originalEnv[$_] = $value + Remove-Item -LiteralPath "Env:\$_" + } + } + + $compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units) + } + finally { + foreach ($kvp in $originalEnv.GetEnumerator()) { + [System.Environment]::SetEnvironmentVariable($kvp.Key, $kvp.Value, "Process") + } + } + + if ($compile.Errors.HasErrors) { + $msg = "Failed to compile C# code: " + foreach ($e in $compile.Errors) { + $msg += "`r`n" + $e.ToString() + } + throw [InvalidOperationException]$msg + } + $compiled_assembly = $compile.CompiledAssembly + } + + $type_accelerator = [PSObject].Assembly.GetType("System.Management.Automation.TypeAccelerators") + foreach ($accelerator in $type_accelerators) { + $type_name = $accelerator.TypeName + $found = $false + + foreach ($assembly_type in $compiled_assembly.GetTypes()) { + if ($assembly_type.Name -eq $type_name) { + $type_accelerator::Add($accelerator.Name, $assembly_type) + $found = $true + break + } + } + if (-not $found) { + throw "Failed to find compiled class '$type_name' for custom TypeAccelerator." + } + } + + # return the compiled assembly if PassThru is set. + if ($PassThru) { + return $compiled_assembly + } +} + +Export-ModuleMember -Function Add-CSharpType + diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 new file mode 100644 index 0000000..53d6870 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 @@ -0,0 +1,78 @@ +# Copyright (c) 2017 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +# The rules used in these functions are derived from the below +# https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments +# https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + +Function Escape-Argument($argument, $force_quote = $false) { + # this converts a single argument to an escaped version, use Join-Arguments + # instead of this function as this only escapes a single string. + + # check if argument contains a space, \n, \t, \v or " + if ($force_quote -eq $false -and $argument.Length -gt 0 -and $argument -notmatch "[ \n\t\v`"]") { + # argument does not need escaping (and we don't want to force it), + # return as is + return $argument + } + else { + # we need to quote the arg so start with " + $new_argument = '"' + + for ($i = 0; $i -lt $argument.Length; $i++) { + $num_backslashes = 0 + + # get the number of \ from current char until end or not a \ + while ($i -ne ($argument.Length - 1) -and $argument[$i] -eq "\") { + $num_backslashes++ + $i++ + } + + $current_char = $argument[$i] + if ($i -eq ($argument.Length - 1) -and $current_char -eq "\") { + # We are at the end of the string so we need to add the same \ + # * 2 as the end char would be a " + $new_argument += ("\" * ($num_backslashes + 1) * 2) + } + elseif ($current_char -eq '"') { + # we have a inline ", we need to add the existing \ but * by 2 + # plus another 1 + $new_argument += ("\" * (($num_backslashes * 2) + 1)) + $new_argument += $current_char + } + else { + # normal character so no need to escape the \ we have counted + $new_argument += ("\" * $num_backslashes) + $new_argument += $current_char + } + } + + # we need to close the special arg with a " + $new_argument += '"' + return $new_argument + } +} + +Function Argv-ToString($arguments, $force_quote = $false) { + # Takes in a list of un escaped arguments and convert it to a single string + # that can be used when starting a new process. It will escape the + # characters as necessary in the list. + # While there is a CommandLineToArgvW function there is a no + # ArgvToCommandLineW that we can call to convert a list to an escaped + # string. + # You can also pass in force_quote so that each argument is quoted even + # when not necessary, by default only arguments with certain characters are + # quoted. + # TODO: add in another switch which will escape the args for cmd.exe + + $escaped_arguments = @() + foreach ($argument in $arguments) { + $escaped_argument = Escape-Argument -argument $argument -force_quote $force_quote + $escaped_arguments += $escaped_argument + } + + return ($escaped_arguments -join ' ') +} + +# this line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Alias * -Function * -Cmdlet * diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Backup.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Backup.psm1 new file mode 100644 index 0000000..ca4f5ba --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Backup.psm1 @@ -0,0 +1,34 @@ +# Copyright (c): 2018, Dag Wieers (@dagwieers) <dag@wieers.com> +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +Function Backup-File { + <# + .SYNOPSIS + Helper function to make a backup of a file. + .EXAMPLE + Backup-File -path $path -WhatIf:$check_mode +#> + [CmdletBinding(SupportsShouldProcess = $true)] + + Param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] $path + ) + + Process { + $backup_path = $null + if (Test-Path -LiteralPath $path -PathType Leaf) { + $backup_path = "$path.$pid." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss") + ".bak"; + Try { + Copy-Item -LiteralPath $path -Destination $backup_path + } + Catch { + throw "Failed to create backup file '$backup_path' from '$path'. ($($_.Exception.Message))" + } + } + return $backup_path + } +} + +# This line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Function Backup-File diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 new file mode 100644 index 0000000..9b86f84 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 @@ -0,0 +1,69 @@ +# Copyright (c) 2017 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +# used by Convert-DictToSnakeCase to convert a string in camelCase +# format to snake_case +Function Convert-StringToSnakeCase($string) { + # cope with pluralized abbreaviations such as TargetGroupARNs + if ($string -cmatch "[A-Z]{3,}s") { + $replacement_string = $string -creplace $matches[0], "_$($matches[0].ToLower())" + + # handle when there was nothing before the plural pattern + if ($replacement_string.StartsWith("_") -and -not $string.StartsWith("_")) { + $replacement_string = $replacement_string.Substring(1) + } + $string = $replacement_string + } + $string = $string -creplace "(.)([A-Z][a-z]+)", '$1_$2' + $string = $string -creplace "([a-z0-9])([A-Z])", '$1_$2' + $string = $string.ToLower() + + return $string +} + +# used by Convert-DictToSnakeCase to covert list entries from camelCase +# to snake_case +Function Convert-ListToSnakeCase($list) { + $snake_list = [System.Collections.ArrayList]@() + foreach ($value in $list) { + if ($value -is [Hashtable]) { + $new_value = Convert-DictToSnakeCase -dict $value + } + elseif ($value -is [Array] -or $value -is [System.Collections.ArrayList]) { + $new_value = Convert-ListToSnakeCase -list $value + } + else { + $new_value = $value + } + [void]$snake_list.Add($new_value) + } + + return , $snake_list +} + +# converts a dict/hashtable keys from camelCase to snake_case +# this is to keep the return values consistent with the Ansible +# way of working. +Function Convert-DictToSnakeCase($dict) { + $snake_dict = @{} + foreach ($dict_entry in $dict.GetEnumerator()) { + $key = $dict_entry.Key + $snake_key = Convert-StringToSnakeCase -string $key + + $value = $dict_entry.Value + if ($value -is [Hashtable]) { + $snake_dict.$snake_key = Convert-DictToSnakeCase -dict $value + } + elseif ($value -is [Array] -or $value -is [System.Collections.ArrayList]) { + $snake_dict.$snake_key = Convert-ListToSnakeCase -list $value + } + else { + $snake_dict.$snake_key = $value + } + } + + return , $snake_dict +} + +# this line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Alias * -Function * -Cmdlet * diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 new file mode 100644 index 0000000..56b5d39 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 @@ -0,0 +1,107 @@ +# Copyright (c) 2017 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +#AnsibleRequires -CSharpUtil Ansible.Process + +Function Get-ExecutablePath { + <# + .SYNOPSIS + Get's the full path to an executable, will search the directory specified or ones in the PATH env var. + + .PARAMETER executable + [String]The executable to search for. + + .PARAMETER directory + [String] If set, the directory to search in. + + .OUTPUT + [String] The full path the executable specified. + #> + Param( + [String]$executable, + [String]$directory = $null + ) + + # we need to add .exe if it doesn't have an extension already + if (-not [System.IO.Path]::HasExtension($executable)) { + $executable = "$($executable).exe" + } + $full_path = [System.IO.Path]::GetFullPath($executable) + + if ($full_path -ne $executable -and $directory -ne $null) { + $file = Get-Item -LiteralPath "$directory\$executable" -Force -ErrorAction SilentlyContinue + } + else { + $file = Get-Item -LiteralPath $executable -Force -ErrorAction SilentlyContinue + } + + if ($null -ne $file) { + $executable_path = $file.FullName + } + else { + $executable_path = [Ansible.Process.ProcessUtil]::SearchPath($executable) + } + return $executable_path +} + +Function Run-Command { + <# + .SYNOPSIS + Run a command with the CreateProcess API and return the stdout/stderr and return code. + + .PARAMETER command + The full command, including the executable, to run. + + .PARAMETER working_directory + The working directory to set on the new process, will default to the current working dir. + + .PARAMETER stdin + A string to sent over the stdin pipe to the new process. + + .PARAMETER environment + A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars. + + .PARAMETER output_encoding_override + The character encoding name for decoding stdout/stderr output of the process. + + .OUTPUT + [Hashtable] + [String]executable - The full path to the executable that was run + [String]stdout - The stdout stream of the process + [String]stderr - The stderr stream of the process + [Int32]rc - The return code of the process + #> + Param( + [string]$command, + [string]$working_directory = $null, + [string]$stdin = "", + [hashtable]$environment = @{}, + [string]$output_encoding_override = $null + ) + + # need to validate the working directory if it is set + if ($working_directory) { + # validate working directory is a valid path + if (-not (Test-Path -LiteralPath $working_directory)) { + throw "invalid working directory path '$working_directory'" + } + } + + # lpApplicationName needs to be the full path to an executable, we do this + # by getting the executable as the first arg and then getting the full path + $arguments = [Ansible.Process.ProcessUtil]::ParseCommandLine($command) + $executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory + + # run the command and get the results + $command_result = [Ansible.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin, $output_encoding_override) + + return , @{ + executable = $executable + stdout = $command_result.StandardOut + stderr = $command_result.StandardError + rc = $command_result.ExitCode + } +} + +# this line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Function Get-ExecutablePath, Run-Command diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 new file mode 100644 index 0000000..cd614d4 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 @@ -0,0 +1,66 @@ +# Copyright (c) 2017 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +<# +Test-Path/Get-Item cannot find/return info on files that are locked like +C:\pagefile.sys. These 2 functions are designed to work with these files and +provide similar functionality with the normal cmdlets with as minimal overhead +as possible. They work by using Get-ChildItem with a filter and return the +result from that. +#> + +Function Test-AnsiblePath { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)][string]$Path + ) + # Replacement for Test-Path + try { + $file_attributes = [System.IO.File]::GetAttributes($Path) + } + catch [System.IO.FileNotFoundException], [System.IO.DirectoryNotFoundException] { + return $false + } + catch [NotSupportedException] { + # When testing a path like Cert:\LocalMachine\My, System.IO.File will + # not work, we just revert back to using Test-Path for this + return Test-Path -Path $Path + } + + if ([Int32]$file_attributes -eq -1) { + return $false + } + else { + return $true + } +} + +Function Get-AnsibleItem { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)][string]$Path + ) + # Replacement for Get-Item + try { + $file_attributes = [System.IO.File]::GetAttributes($Path) + } + catch { + # if -ErrorAction SilentlyCotinue is set on the cmdlet and we failed to + # get the attributes, just return $null, otherwise throw the error + if ($ErrorActionPreference -ne "SilentlyContinue") { + throw $_ + } + return $null + } + if ([Int32]$file_attributes -eq -1) { + throw New-Object -TypeName System.Management.Automation.ItemNotFoundException -ArgumentList "Cannot find path '$Path' because it does not exist." + } + elseif ($file_attributes.HasFlag([System.IO.FileAttributes]::Directory)) { + return New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $Path + } + else { + return New-Object -TypeName System.IO.FileInfo -ArgumentList $Path + } +} + +Export-ModuleMember -Function Test-AnsiblePath, Get-AnsibleItem diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 new file mode 100644 index 0000000..f0cb440 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 @@ -0,0 +1,390 @@ +# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2014, and others +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = "Stop" + +Function Set-Attr($obj, $name, $value) { + <# + .SYNOPSIS + Helper function to set an "attribute" on a psobject instance in PowerShell. + This is a convenience to make adding Members to the object easier and + slightly more pythonic + .EXAMPLE + Set-Attr $result "changed" $true +#> + + # If the provided $obj is undefined, define one to be nice + If (-not $obj.GetType) { + $obj = @{ } + } + + Try { + $obj.$name = $value + } + Catch { + $obj | Add-Member -Force -MemberType NoteProperty -Name $name -Value $value + } +} + +Function Exit-Json($obj) { + <# + .SYNOPSIS + Helper function to convert a PowerShell object to JSON and output it, exiting + the script + .EXAMPLE + Exit-Json $result +#> + + # If the provided $obj is undefined, define one to be nice + If (-not $obj.GetType) { + $obj = @{ } + } + + if (-not $obj.ContainsKey('changed')) { + Set-Attr -obj $obj -name "changed" -value $false + } + + Write-Output $obj | ConvertTo-Json -Compress -Depth 99 + Exit +} + +Function Fail-Json($obj, $message = $null) { + <# + .SYNOPSIS + Helper function to add the "msg" property and "failed" property, convert the + PowerShell Hashtable to JSON and output it, exiting the script + .EXAMPLE + Fail-Json $result "This is the failure message" +#> + + if ($obj -is [hashtable] -or $obj -is [psobject]) { + # Nothing to do + } + elseif ($obj -is [string] -and $null -eq $message) { + # If we weren't given 2 args, and the only arg was a string, + # create a new Hashtable and use the arg as the failure message + $message = $obj + $obj = @{ } + } + else { + # If the first argument is undefined or a different type, + # make it a Hashtable + $obj = @{ } + } + + # Still using Set-Attr for PSObject compatibility + Set-Attr -obj $obj -name "msg" -value $message + Set-Attr -obj $obj -name "failed" -value $true + + if (-not $obj.ContainsKey('changed')) { + Set-Attr -obj $obj -name "changed" -value $false + } + + Write-Output $obj | ConvertTo-Json -Compress -Depth 99 + Exit 1 +} + +Function Add-Warning($obj, $message) { + <# + .SYNOPSIS + Helper function to add warnings, even if the warnings attribute was + not already set up. This is a convenience for the module developer + so they do not have to check for the attribute prior to adding. +#> + + if (-not $obj.ContainsKey("warnings")) { + $obj.warnings = @() + } + elseif ($obj.warnings -isnot [array]) { + throw "Add-Warning: warnings attribute is not an array" + } + + $obj.warnings += $message +} + +Function Add-DeprecationWarning($obj, $message, $version = $null) { + <# + .SYNOPSIS + Helper function to add deprecations, even if the deprecations attribute was + not already set up. This is a convenience for the module developer + so they do not have to check for the attribute prior to adding. +#> + if (-not $obj.ContainsKey("deprecations")) { + $obj.deprecations = @() + } + elseif ($obj.deprecations -isnot [array]) { + throw "Add-DeprecationWarning: deprecations attribute is not a list" + } + + $obj.deprecations += @{ + msg = $message + version = $version + } +} + +Function Expand-Environment($value) { + <# + .SYNOPSIS + Helper function to expand environment variables in values. By default + it turns any type to a string, but we ensure $null remains $null. +#> + if ($null -ne $value) { + [System.Environment]::ExpandEnvironmentVariables($value) + } + else { + $value + } +} + +Function Get-AnsibleParam { + <# + .SYNOPSIS + Helper function to get an "attribute" from a psobject instance in PowerShell. + This is a convenience to make getting Members from an object easier and + slightly more pythonic + .EXAMPLE + $attr = Get-AnsibleParam $response "code" -default "1" + .EXAMPLE + Get-AnsibleParam -obj $params -name "State" -default "Present" -ValidateSet "Present","Absent" -resultobj $resultobj -failifempty $true + Get-AnsibleParam also supports Parameter validation to save you from coding that manually + Note that if you use the failifempty option, you do need to specify resultobject as well. +#> + param ( + $obj, + $name, + $default = $null, + $resultobj = @{}, + $failifempty = $false, + $emptyattributefailmessage, + $ValidateSet, + $ValidateSetErrorMessage, + $type = $null, + $aliases = @() + ) + # Check if the provided Member $name or aliases exist in $obj and return it or the default. + try { + + $found = $null + # First try to find preferred parameter $name + $aliases = @($name) + $aliases + + # Iterate over aliases to find acceptable Member $name + foreach ($alias in $aliases) { + if ($obj.ContainsKey($alias)) { + $found = $alias + break + } + } + + if ($null -eq $found) { + throw + } + $name = $found + + if ($ValidateSet) { + + if ($ValidateSet -contains ($obj.$name)) { + $value = $obj.$name + } + else { + if ($null -eq $ValidateSetErrorMessage) { + #Auto-generated error should be sufficient in most use cases + $ValidateSetErrorMessage = "Get-AnsibleParam: Argument $name needs to be one of $($ValidateSet -join ",") but was $($obj.$name)." + } + Fail-Json -obj $resultobj -message $ValidateSetErrorMessage + } + } + else { + $value = $obj.$name + } + } + catch { + if ($failifempty -eq $false) { + $value = $default + } + else { + if (-not $emptyattributefailmessage) { + $emptyattributefailmessage = "Get-AnsibleParam: Missing required argument: $name" + } + Fail-Json -obj $resultobj -message $emptyattributefailmessage + } + } + + # If $null -eq $value, the parameter was unspecified by the user (deliberately or not) + # Please leave $null-values intact, modules need to know if a parameter was specified + if ($null -eq $value) { + return $null + } + + if ($type -eq "path") { + # Expand environment variables on path-type + $value = Expand-Environment($value) + # Test if a valid path is provided + if (-not (Test-Path -IsValid $value)) { + $path_invalid = $true + # could still be a valid-shaped path with a nonexistent drive letter + if ($value -match "^\w:") { + # rewrite path with a valid drive letter and recheck the shape- this might still fail, eg, a nonexistent non-filesystem PS path + if (Test-Path -IsValid $(@(Get-PSDrive -PSProvider Filesystem)[0].Name + $value.Substring(1))) { + $path_invalid = $false + } + } + if ($path_invalid) { + Fail-Json -obj $resultobj -message "Get-AnsibleParam: Parameter '$name' has an invalid path '$value' specified." + } + } + } + elseif ($type -eq "str") { + # Convert str types to real Powershell strings + $value = $value.ToString() + } + elseif ($type -eq "bool") { + # Convert boolean types to real Powershell booleans + $value = $value | ConvertTo-Bool + } + elseif ($type -eq "int") { + # Convert int types to real Powershell integers + $value = $value -as [int] + } + elseif ($type -eq "float") { + # Convert float types to real Powershell floats + $value = $value -as [float] + } + elseif ($type -eq "list") { + if ($value -is [array]) { + # Nothing to do + } + elseif ($value -is [string]) { + # Convert string type to real Powershell array + $value = $value.Split(",").Trim() + } + elseif ($value -is [int]) { + $value = @($value) + } + else { + Fail-Json -obj $resultobj -message "Get-AnsibleParam: Parameter '$name' is not a YAML list." + } + # , is not a typo, forces it to return as a list when it is empty or only has 1 entry + return , $value + } + + return $value +} + +#Alias Get-attr-->Get-AnsibleParam for backwards compat. Only add when needed to ease debugging of scripts +If (-not(Get-Alias -Name "Get-attr" -ErrorAction SilentlyContinue)) { + New-Alias -Name Get-attr -Value Get-AnsibleParam +} + +Function ConvertTo-Bool { + <# + .SYNOPSIS + Helper filter/pipeline function to convert a value to boolean following current + Ansible practices + .EXAMPLE + $is_true = "true" | ConvertTo-Bool +#> + param( + [parameter(valuefrompipeline = $true)] + $obj + ) + + process { + $boolean_strings = "yes", "on", "1", "true", 1 + $obj_string = [string]$obj + + if (($obj -is [boolean] -and $obj) -or $boolean_strings -contains $obj_string.ToLower()) { + return $true + } + else { + return $false + } + } +} + +Function Parse-Args { + <# + .SYNOPSIS + Helper function to parse Ansible JSON arguments from a "file" passed as + the single argument to the module. + .EXAMPLE + $params = Parse-Args $args +#> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Cannot change the name now")] + param ($arguments, $supports_check_mode = $false) + + $params = New-Object psobject + If ($arguments.Length -gt 0) { + $params = Get-Content $arguments[0] | ConvertFrom-Json + } + Else { + $params = $complex_args + } + $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false + If ($check_mode -and -not $supports_check_mode) { + Exit-Json @{ + skipped = $true + changed = $false + msg = "remote module does not support check mode" + } + } + return $params +} + + +Function Get-FileChecksum($path, $algorithm = 'sha1') { + <# + .SYNOPSIS + Helper function to calculate a hash of a file in a way which PowerShell 3 + and above can handle +#> + If (Test-Path -LiteralPath $path -PathType Leaf) { + switch ($algorithm) { + 'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider } + 'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider } + 'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider } + 'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider } + 'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider } + default { Fail-Json @{} "Unsupported hash algorithm supplied '$algorithm'" } + } + + If ($PSVersionTable.PSVersion.Major -ge 4) { + $raw_hash = Get-FileHash -LiteralPath $path -Algorithm $algorithm + $hash = $raw_hash.Hash.ToLower() + } + Else { + $fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite); + $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower(); + $fp.Dispose(); + } + } + ElseIf (Test-Path -LiteralPath $path -PathType Container) { + $hash = "3"; + } + Else { + $hash = "1"; + } + return $hash +} + +Function Get-PendingRebootStatus { + <# + .SYNOPSIS + Check if reboot is required, if so notify CA. + Function returns true if computer has a pending reboot +#> + $featureData = Invoke-CimMethod -EA Ignore -Name GetServerFeature -Namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks + $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore + $CBSRebootStatus = Get-ChildItem "HKLM:\\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing" -ErrorAction SilentlyContinue | + Where-Object { $_.PSChildName -eq "RebootPending" } + if (($featureData -and $featureData.RequiresReboot) -or $regData -or $CBSRebootStatus) { + return $True + } + else { + return $False + } +} + +# this line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Alias * -Function * -Cmdlet * diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.LinkUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.LinkUtil.psm1 new file mode 100644 index 0000000..1a251f6 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.LinkUtil.psm1 @@ -0,0 +1,464 @@ +# Copyright (c) 2017 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +#Requires -Module Ansible.ModuleUtils.PrivilegeUtil + +Function Load-LinkUtils { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification = "Cannot change the name now")] + param () + + $link_util = @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible +{ + public enum LinkType + { + SymbolicLink, + JunctionPoint, + HardLink + } + + public class LinkUtilWin32Exception : System.ComponentModel.Win32Exception + { + private string _msg; + + public LinkUtilWin32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + + public LinkUtilWin32Exception(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator LinkUtilWin32Exception(string message) { return new LinkUtilWin32Exception(message); } + } + + public class LinkInfo + { + public LinkType Type { get; internal set; } + public string PrintName { get; internal set; } + public string SubstituteName { get; internal set; } + public string AbsolutePath { get; internal set; } + public string TargetPath { get; internal set; } + public string[] HardTargets { get; internal set; } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct REPARSE_DATA_BUFFER + { + public UInt32 ReparseTag; + public UInt16 ReparseDataLength; + public UInt16 Reserved; + public UInt16 SubstituteNameOffset; + public UInt16 SubstituteNameLength; + public UInt16 PrintNameOffset; + public UInt16 PrintNameLength; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = LinkUtil.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)] + public char[] PathBuffer; + } + + public class LinkUtil + { + public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 1024 * 16; + + private const UInt32 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + private const UInt32 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000; + + private const UInt32 FSCTL_GET_REPARSE_POINT = 0x000900A8; + private const UInt32 FSCTL_SET_REPARSE_POINT = 0x000900A4; + private const UInt32 FILE_DEVICE_FILE_SYSTEM = 0x00090000; + + private const UInt32 IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; + private const UInt32 IO_REPARSE_TAG_SYMLINK = 0xA000000C; + + private const UInt32 SYMLINK_FLAG_RELATIVE = 0x00000001; + + private const Int64 INVALID_HANDLE_VALUE = -1; + + private const UInt32 SIZE_OF_WCHAR = 2; + + private const UInt32 SYMBOLIC_LINK_FLAG_FILE = 0x00000000; + private const UInt32 SYMBOLIC_LINK_FLAG_DIRECTORY = 0x00000001; + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern SafeFileHandle CreateFile( + string lpFileName, + [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, + [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode, + IntPtr lpSecurityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition, + UInt32 dwFlagsAndAttributes, + IntPtr hTemplateFile); + + // Used by GetReparsePointInfo() + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeviceIoControl( + SafeFileHandle hDevice, + UInt32 dwIoControlCode, + IntPtr lpInBuffer, + UInt32 nInBufferSize, + out REPARSE_DATA_BUFFER lpOutBuffer, + UInt32 nOutBufferSize, + out UInt32 lpBytesReturned, + IntPtr lpOverlapped); + + // Used by CreateJunctionPoint() + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeviceIoControl( + SafeFileHandle hDevice, + UInt32 dwIoControlCode, + REPARSE_DATA_BUFFER lpInBuffer, + UInt32 nInBufferSize, + IntPtr lpOutBuffer, + UInt32 nOutBufferSize, + out UInt32 lpBytesReturned, + IntPtr lpOverlapped); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool GetVolumePathName( + string lpszFileName, + StringBuilder lpszVolumePathName, + ref UInt32 cchBufferLength); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr FindFirstFileNameW( + string lpFileName, + UInt32 dwFlags, + ref UInt32 StringLength, + StringBuilder LinkName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool FindNextFileNameW( + IntPtr hFindStream, + ref UInt32 StringLength, + StringBuilder LinkName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool FindClose( + IntPtr hFindFile); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool RemoveDirectory( + string lpPathName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeleteFile( + string lpFileName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool CreateSymbolicLink( + string lpSymlinkFileName, + string lpTargetFileName, + UInt32 dwFlags); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool CreateHardLink( + string lpFileName, + string lpExistingFileName, + IntPtr lpSecurityAttributes); + + public static LinkInfo GetLinkInfo(string linkPath) + { + FileAttributes attr = File.GetAttributes(linkPath); + if (attr.HasFlag(FileAttributes.ReparsePoint)) + return GetReparsePointInfo(linkPath); + + if (!attr.HasFlag(FileAttributes.Directory)) + return GetHardLinkInfo(linkPath); + + return null; + } + + public static void DeleteLink(string linkPath) + { + bool success; + FileAttributes attr = File.GetAttributes(linkPath); + if (attr.HasFlag(FileAttributes.Directory)) + { + success = RemoveDirectory(linkPath); + } + else + { + success = DeleteFile(linkPath); + } + + if (!success) + throw new LinkUtilWin32Exception(String.Format("Failed to delete link at {0}", linkPath)); + } + + public static void CreateLink(string linkPath, String linkTarget, LinkType linkType) + { + switch (linkType) + { + case LinkType.SymbolicLink: + UInt32 linkFlags; + FileAttributes attr = File.GetAttributes(linkTarget); + if (attr.HasFlag(FileAttributes.Directory)) + linkFlags = SYMBOLIC_LINK_FLAG_DIRECTORY; + else + linkFlags = SYMBOLIC_LINK_FLAG_FILE; + + if (!CreateSymbolicLink(linkPath, linkTarget, linkFlags)) + throw new LinkUtilWin32Exception(String.Format("CreateSymbolicLink({0}, {1}, {2}) failed", linkPath, linkTarget, linkFlags)); + break; + case LinkType.JunctionPoint: + CreateJunctionPoint(linkPath, linkTarget); + break; + case LinkType.HardLink: + if (!CreateHardLink(linkPath, linkTarget, IntPtr.Zero)) + throw new LinkUtilWin32Exception(String.Format("CreateHardLink({0}, {1}) failed", linkPath, linkTarget)); + break; + } + } + + private static LinkInfo GetHardLinkInfo(string linkPath) + { + UInt32 maxPath = 260; + List<string> result = new List<string>(); + + StringBuilder sb = new StringBuilder((int)maxPath); + UInt32 stringLength = maxPath; + if (!GetVolumePathName(linkPath, sb, ref stringLength)) + throw new LinkUtilWin32Exception("GetVolumePathName() failed"); + string volume = sb.ToString(); + + stringLength = maxPath; + IntPtr findHandle = FindFirstFileNameW(linkPath, 0, ref stringLength, sb); + if (findHandle.ToInt64() != INVALID_HANDLE_VALUE) + { + try + { + do + { + string hardLinkPath = sb.ToString(); + if (hardLinkPath.StartsWith("\\")) + hardLinkPath = hardLinkPath.Substring(1, hardLinkPath.Length - 1); + + result.Add(Path.Combine(volume, hardLinkPath)); + stringLength = maxPath; + + } while (FindNextFileNameW(findHandle, ref stringLength, sb)); + } + finally + { + FindClose(findHandle); + } + } + + if (result.Count > 1) + return new LinkInfo + { + Type = LinkType.HardLink, + HardTargets = result.ToArray() + }; + + return null; + } + + private static LinkInfo GetReparsePointInfo(string linkPath) + { + SafeFileHandle fileHandle = CreateFile( + linkPath, + FileAccess.Read, + FileShare.None, + IntPtr.Zero, + FileMode.Open, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + if (fileHandle.IsInvalid) + throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath)); + + REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER(); + UInt32 bytesReturned; + try + { + if (!DeviceIoControl( + fileHandle, + FSCTL_GET_REPARSE_POINT, + IntPtr.Zero, + 0, + out buffer, + MAXIMUM_REPARSE_DATA_BUFFER_SIZE, + out bytesReturned, + IntPtr.Zero)) + throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed for file at {0}", linkPath)); + } + finally + { + fileHandle.Dispose(); + } + + bool isRelative = false; + int pathOffset = 0; + LinkType linkType; + if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK) + { + UInt32 bufferFlags = Convert.ToUInt32(buffer.PathBuffer[0]) + Convert.ToUInt32(buffer.PathBuffer[1]); + if (bufferFlags == SYMLINK_FLAG_RELATIVE) + isRelative = true; + pathOffset = 2; + linkType = LinkType.SymbolicLink; + } + else if (buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) + { + linkType = LinkType.JunctionPoint; + } + else + { + string errorMessage = String.Format("Invalid Reparse Tag: {0}", buffer.ReparseTag.ToString()); + throw new Exception(errorMessage); + } + + string printName = new string(buffer.PathBuffer, + (int)(buffer.PrintNameOffset / SIZE_OF_WCHAR) + pathOffset, + (int)(buffer.PrintNameLength / SIZE_OF_WCHAR)); + string substituteName = new string(buffer.PathBuffer, + (int)(buffer.SubstituteNameOffset / SIZE_OF_WCHAR) + pathOffset, + (int)(buffer.SubstituteNameLength / SIZE_OF_WCHAR)); + + // TODO: should we check for \?\UNC\server for convert it to the NT style \\server path + // Remove the leading Windows object directory \?\ from the path if present + string targetPath = substituteName; + if (targetPath.StartsWith("\\??\\")) + targetPath = targetPath.Substring(4, targetPath.Length - 4); + + string absolutePath = targetPath; + if (isRelative) + absolutePath = Path.GetFullPath(Path.Combine(new FileInfo(linkPath).Directory.FullName, targetPath)); + + return new LinkInfo + { + Type = linkType, + PrintName = printName, + SubstituteName = substituteName, + AbsolutePath = absolutePath, + TargetPath = targetPath + }; + } + + private static void CreateJunctionPoint(string linkPath, string linkTarget) + { + // We need to create the link as a dir beforehand + Directory.CreateDirectory(linkPath); + SafeFileHandle fileHandle = CreateFile( + linkPath, + FileAccess.Write, + FileShare.Read | FileShare.Write | FileShare.None, + IntPtr.Zero, + FileMode.Open, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + IntPtr.Zero); + + if (fileHandle.IsInvalid) + throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath)); + + try + { + string substituteName = "\\??\\" + Path.GetFullPath(linkTarget); + string printName = linkTarget; + + REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER(); + buffer.SubstituteNameOffset = 0; + buffer.SubstituteNameLength = (UInt16)(substituteName.Length * SIZE_OF_WCHAR); + buffer.PrintNameOffset = (UInt16)(buffer.SubstituteNameLength + 2); + buffer.PrintNameLength = (UInt16)(printName.Length * SIZE_OF_WCHAR); + + buffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; + buffer.ReparseDataLength = (UInt16)(buffer.SubstituteNameLength + buffer.PrintNameLength + 12); + buffer.PathBuffer = new char[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + + byte[] unicodeBytes = Encoding.Unicode.GetBytes(substituteName + "\0" + printName); + char[] pathBuffer = Encoding.Unicode.GetChars(unicodeBytes); + Array.Copy(pathBuffer, buffer.PathBuffer, pathBuffer.Length); + + UInt32 bytesReturned; + if (!DeviceIoControl( + fileHandle, + FSCTL_SET_REPARSE_POINT, + buffer, + (UInt32)(buffer.ReparseDataLength + 8), + IntPtr.Zero, 0, + out bytesReturned, + IntPtr.Zero)) + throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed to create junction point at {0} to {1}", linkPath, linkTarget)); + } + finally + { + fileHandle.Dispose(); + } + } + } +} +'@ + + # FUTURE: find a better way to get the _ansible_remote_tmp variable + $original_tmp = $env:TMP + $original_lib = $env:LIB + + $remote_tmp = $original_tmp + $module_params = Get-Variable -Name complex_args -ErrorAction SilentlyContinue + if ($module_params) { + if ($module_params.Value.ContainsKey("_ansible_remote_tmp") ) { + $remote_tmp = $module_params.Value["_ansible_remote_tmp"] + $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp) + } + } + + $env:TMP = $remote_tmp + $env:LIB = $null + Add-Type -TypeDefinition $link_util + $env:TMP = $original_tmp + $env:LIB = $original_lib + + # enable the SeBackupPrivilege if it is disabled + $state = Get-AnsiblePrivilege -Name SeBackupPrivilege + if ($state -eq $false) { + Set-AnsiblePrivilege -Name SeBackupPrivilege -Value $true + } +} + +Function Get-Link($link_path) { + $link_info = [Ansible.LinkUtil]::GetLinkInfo($link_path) + return $link_info +} + +Function Remove-Link($link_path) { + [Ansible.LinkUtil]::DeleteLink($link_path) +} + +Function New-Link($link_path, $link_target, $link_type) { + if (-not (Test-Path -LiteralPath $link_target)) { + throw "link_target '$link_target' does not exist, cannot create link" + } + + switch ($link_type) { + "link" { + $type = [Ansible.LinkType]::SymbolicLink + } + "junction" { + if (Test-Path -LiteralPath $link_target -PathType Leaf) { + throw "cannot set the target for a junction point to a file" + } + $type = [Ansible.LinkType]::JunctionPoint + } + "hard" { + if (Test-Path -LiteralPath $link_target -PathType Container) { + throw "cannot set the target for a hard link to a directory" + } + $type = [Ansible.LinkType]::HardLink + } + default { throw "invalid link_type option $($link_type): expecting link, junction, hard" } + } + [Ansible.LinkUtil]::CreateLink($link_path, $link_target, $type) +} + +# this line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Alias * -Function * -Cmdlet * diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.PrivilegeUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.PrivilegeUtil.psm1 new file mode 100644 index 0000000..78f0d64 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.PrivilegeUtil.psm1 @@ -0,0 +1,83 @@ +# Copyright (c) 2018 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +#AnsibleRequires -CSharpUtil Ansible.Privilege + +Function Get-AnsiblePrivilege { + <# + .SYNOPSIS + Get the status of a privilege for the current process. This returns + $true - the privilege is enabled + $false - the privilege is disabled + $null - the privilege is removed from the token + + If Name is not a valid privilege name, this will throw an + ArgumentException. + + .EXAMPLE + Get-AnsiblePrivilege -Name SeDebugPrivilege + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][String]$Name + ) + + if (-not [Ansible.Privilege.PrivilegeUtil]::CheckPrivilegeName($Name)) { + throw [System.ArgumentException] "Invalid privilege name '$Name'" + } + + $process_token = [Ansible.Privilege.PrivilegeUtil]::GetCurrentProcess() + $privilege_info = [Ansible.Privilege.PrivilegeUtil]::GetAllPrivilegeInfo($process_token) + if ($privilege_info.ContainsKey($Name)) { + $status = $privilege_info.$Name + return $status.HasFlag([Ansible.Privilege.PrivilegeAttributes]::Enabled) + } + else { + return $null + } +} + +Function Set-AnsiblePrivilege { + <# + .SYNOPSIS + Enables/Disables a privilege on the current process' token. If a privilege + has been removed from the process token, this will throw an + InvalidOperationException. + + .EXAMPLE + # enable a privilege + Set-AnsiblePrivilege -Name SeCreateSymbolicLinkPrivilege -Value $true + + # disable a privilege + Set-AnsiblePrivilege -Name SeCreateSymbolicLinkPrivilege -Value $false + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)][String]$Name, + [Parameter(Mandatory = $true)][bool]$Value + ) + + $action = switch ($Value) { + $true { "Enable" } + $false { "Disable" } + } + + $current_state = Get-AnsiblePrivilege -Name $Name + if ($current_state -eq $Value) { + return # no change needs to occur + } + elseif ($null -eq $current_state) { + # once a privilege is removed from a token we cannot do anything with it + throw [System.InvalidOperationException] "Cannot $($action.ToLower()) the privilege '$Name' as it has been removed from the token" + } + + $process_token = [Ansible.Privilege.PrivilegeUtil]::GetCurrentProcess() + if ($PSCmdlet.ShouldProcess($Name, "$action the privilege $Name")) { + $new_state = New-Object -TypeName 'System.Collections.Generic.Dictionary`2[[System.String], [System.Nullable`1[System.Boolean]]]' + $new_state.Add($Name, $Value) + [Ansible.Privilege.PrivilegeUtil]::SetTokenPrivileges($process_token, $new_state) > $null + } +} + +Export-ModuleMember -Function Get-AnsiblePrivilege, Set-AnsiblePrivilege + diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.SID.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.SID.psm1 new file mode 100644 index 0000000..d1f4b62 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.SID.psm1 @@ -0,0 +1,99 @@ +# Copyright (c) 2017 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +Function Convert-FromSID($sid) { + # Converts a SID to a Down-Level Logon name in the form of DOMAIN\UserName + # If the SID is for a local user or group then DOMAIN would be the server + # name. + + $account_object = New-Object System.Security.Principal.SecurityIdentifier($sid) + try { + $nt_account = $account_object.Translate([System.Security.Principal.NTAccount]) + } + catch { + Fail-Json -obj @{} -message "failed to convert sid '$sid' to a logon name: $($_.Exception.Message)" + } + + return $nt_account.Value +} + +Function Convert-ToSID { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", + Justification = "We don't care if converting to a SID fails, just that it failed or not")] + param($account_name) + # Converts an account name to a SID, it can take in the following forms + # SID: Will just return the SID value that was passed in + # UPN: + # principal@domain (Domain users only) + # Down-Level Login Name + # DOMAIN\principal (Domain) + # SERVERNAME\principal (Local) + # .\principal (Local) + # NT AUTHORITY\SYSTEM (Local Service Accounts) + # Login Name + # principal (Local/Local Service Accounts) + + try { + $sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $account_name + return $sid.Value + } + catch {} + + if ($account_name -like "*\*") { + $account_name_split = $account_name -split "\\" + if ($account_name_split[0] -eq ".") { + $domain = $env:COMPUTERNAME + } + else { + $domain = $account_name_split[0] + } + $username = $account_name_split[1] + } + else { + $domain = $null + $username = $account_name + } + + if ($domain) { + # searching for a local group with the servername prefixed will fail, + # need to check for this situation and only use NTAccount(String) + if ($domain -eq $env:COMPUTERNAME) { + $adsi = [ADSI]("WinNT://$env:COMPUTERNAME,computer") + $group = $adsi.psbase.children | Where-Object { $_.schemaClassName -eq "group" -and $_.Name -eq $username } + } + else { + $group = $null + } + if ($group) { + $account = New-Object System.Security.Principal.NTAccount($username) + } + else { + $account = New-Object System.Security.Principal.NTAccount($domain, $username) + } + } + else { + # when in a domain NTAccount(String) will favour domain lookups check + # if username is a local user and explicitly search on the localhost for + # that account + $adsi = [ADSI]("WinNT://$env:COMPUTERNAME,computer") + $user = $adsi.psbase.children | Where-Object { $_.schemaClassName -eq "user" -and $_.Name -eq $username } + if ($user) { + $account = New-Object System.Security.Principal.NTAccount($env:COMPUTERNAME, $username) + } + else { + $account = New-Object System.Security.Principal.NTAccount($username) + } + } + + try { + $account_sid = $account.Translate([System.Security.Principal.SecurityIdentifier]) + } + catch { + Fail-Json @{} "account_name $account_name is not a valid account, cannot get SID: $($_.Exception.Message)" + } + + return $account_sid.Value +} + +# this line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Alias * -Function * -Cmdlet * diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 new file mode 100644 index 0000000..b59ba72 --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 @@ -0,0 +1,530 @@ +# Copyright (c) 2019 Ansible Project +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +Function Get-AnsibleWebRequest { + <# + .SYNOPSIS + Creates a System.Net.WebRequest object based on common URL module options in Ansible. + + .DESCRIPTION + Will create a WebRequest based on common input options within Ansible. This can be used manually or with + Invoke-WithWebRequest. + + .PARAMETER Uri + The URI to create the web request for. + + .PARAMETER Method + The protocol method to use, if omitted, will use the default value for the URI protocol specified. + + .PARAMETER FollowRedirects + Whether to follow redirect reponses. This is only valid when using a HTTP URI. + all - Will follow all redirects + none - Will follow no redirects + safe - Will only follow redirects when GET or HEAD is used as the Method + + .PARAMETER Headers + A hashtable or dictionary of header values to set on the request. This is only valid for a HTTP URI. + + .PARAMETER HttpAgent + A string to set for the 'User-Agent' header. This is only valid for a HTTP URI. + + .PARAMETER MaximumRedirection + The maximum number of redirections that will be followed. This is only valid for a HTTP URI. + + .PARAMETER Timeout + The timeout in seconds that defines how long to wait until the request times out. + + .PARAMETER ValidateCerts + Whether to validate SSL certificates, default to True. + + .PARAMETER ClientCert + The path to PFX file to use for X509 authentication. This is only valid for a HTTP URI. This path can either + be a filesystem path (C:\folder\cert.pfx) or a PSPath to a credential (Cert:\CurrentUser\My\<thumbprint>). + + .PARAMETER ClientCertPassword + The password for the PFX certificate if required. This is only valid for a HTTP URI. + + .PARAMETER ForceBasicAuth + Whether to set the Basic auth header on the first request instead of when required. This is only valid for a + HTTP URI. + + .PARAMETER UrlUsername + The username to use for authenticating with the target. + + .PARAMETER UrlPassword + The password to use for authenticating with the target. + + .PARAMETER UseDefaultCredential + Whether to use the current user's credentials if available. This will only work when using Become, using SSH with + password auth, or WinRM with CredSSP or Kerberos with credential delegation. + + .PARAMETER UseProxy + Whether to use the default proxy defined in IE (WinINet) for the user or set no proxy at all. This should not + be set to True when ProxyUrl is also defined. + + .PARAMETER ProxyUrl + An explicit proxy server to use for the request instead of relying on the default proxy in IE. This is only + valid for a HTTP URI. + + .PARAMETER ProxyUsername + An optional username to use for proxy authentication. + + .PARAMETER ProxyPassword + The password for ProxyUsername. + + .PARAMETER ProxyUseDefaultCredential + Whether to use the current user's credentials for proxy authentication if available. This will only work when + using Become, using SSH with password auth, or WinRM with CredSSP or Kerberos with credential delegation. + + .PARAMETER Module + The AnsibleBasic module that can be used as a backup parameter source or a way to return warnings back to the + Ansible controller. + + .EXAMPLE + $spec = @{ + options = @{} + } + $module = Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-AnsibleWebRequestSpec)) + + $web_request = Get-AnsibleWebRequest -Module $module + #> + [CmdletBinding()] + [OutputType([System.Net.WebRequest])] + Param ( + [Alias("url")] + [System.Uri] + $Uri, + + [System.String] + $Method, + + [Alias("follow_redirects")] + [ValidateSet("all", "none", "safe")] + [System.String] + $FollowRedirects = "safe", + + [System.Collections.IDictionary] + $Headers, + + [Alias("http_agent")] + [System.String] + $HttpAgent = "ansible-httpget", + + [Alias("maximum_redirection")] + [System.Int32] + $MaximumRedirection = 50, + + [System.Int32] + $Timeout = 30, + + [Alias("validate_certs")] + [System.Boolean] + $ValidateCerts = $true, + + # Credential params + [Alias("client_cert")] + [System.String] + $ClientCert, + + [Alias("client_cert_password")] + [System.String] + $ClientCertPassword, + + [Alias("force_basic_auth")] + [Switch] + $ForceBasicAuth, + + [Alias("url_username")] + [System.String] + $UrlUsername, + + [Alias("url_password")] + [System.String] + $UrlPassword, + + [Alias("use_default_credential")] + [Switch] + $UseDefaultCredential, + + # Proxy params + [Alias("use_proxy")] + [System.Boolean] + $UseProxy = $true, + + [Alias("proxy_url")] + [System.String] + $ProxyUrl, + + [Alias("proxy_username")] + [System.String] + $ProxyUsername, + + [Alias("proxy_password")] + [System.String] + $ProxyPassword, + + [Alias("proxy_use_default_credential")] + [Switch] + $ProxyUseDefaultCredential, + + [ValidateScript({ $_.GetType().FullName -eq 'Ansible.Basic.AnsibleModule' })] + [System.Object] + $Module + ) + + # Set module options for parameters unless they were explicitly passed in. + if ($Module) { + foreach ($param in $PSCmdlet.MyInvocation.MyCommand.Parameters.GetEnumerator()) { + if ($PSBoundParameters.ContainsKey($param.Key)) { + # Was set explicitly we want to use that value + continue + } + + foreach ($alias in @($Param.Key) + $param.Value.Aliases) { + if ($Module.Params.ContainsKey($alias)) { + $var_value = $Module.Params.$alias -as $param.Value.ParameterType + Set-Variable -Name $param.Key -Value $var_value + break + } + } + } + } + + # Disable certificate validation if requested + # FUTURE: set this on ServerCertificateValidationCallback of the HttpWebRequest once .NET 4.5 is the minimum + if (-not $ValidateCerts) { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } + } + + # Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5) + $security_protocols = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::SystemDefault + if ([System.Net.SecurityProtocolType].GetMember("Tls11").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls11 + } + if ([System.Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) { + $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls12 + } + [System.Net.ServicePointManager]::SecurityProtocol = $security_protocols + + $web_request = [System.Net.WebRequest]::Create($Uri) + if ($Method) { + $web_request.Method = $Method + } + $web_request.Timeout = $Timeout * 1000 + + if ($UseDefaultCredential -and $web_request -is [System.Net.HttpWebRequest]) { + $web_request.UseDefaultCredentials = $true + } + elseif ($UrlUsername) { + if ($ForceBasicAuth) { + $auth_value = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UrlUsername, $UrlPassword))) + $web_request.Headers.Add("Authorization", "Basic $auth_value") + } + else { + $credential = New-Object -TypeName System.Net.NetworkCredential -ArgumentList $UrlUsername, $UrlPassword + $web_request.Credentials = $credential + } + } + + if ($ClientCert) { + # Expecting either a filepath or PSPath (Cert:\CurrentUser\My\<thumbprint>) + $cert = Get-Item -LiteralPath $ClientCert -ErrorAction SilentlyContinue + if ($null -eq $cert) { + Write-Error -Message "Client certificate '$ClientCert' does not exist" -Category ObjectNotFound + return + } + + $crypto_ns = 'System.Security.Cryptography.X509Certificates' + if ($cert.PSProvider.Name -ne 'Certificate') { + try { + $cert = New-Object -TypeName "$crypto_ns.X509Certificate2" -ArgumentList @( + $ClientCert, $ClientCertPassword + ) + } + catch [System.Security.Cryptography.CryptographicException] { + Write-Error -Message "Failed to read client certificate at '$ClientCert'" -Exception $_.Exception -Category SecurityError + return + } + } + $web_request.ClientCertificates = New-Object -TypeName "$crypto_ns.X509Certificate2Collection" -ArgumentList @( + $cert + ) + } + + if (-not $UseProxy) { + $proxy = $null + } + elseif ($ProxyUrl) { + $proxy = New-Object -TypeName System.Net.WebProxy -ArgumentList $ProxyUrl, $true + } + else { + $proxy = $web_request.Proxy + } + + # $web_request.Proxy may return $null for a FTP web request. We only set the credentials if we have an actual + # proxy to work with, otherwise just ignore the credentials property. + if ($null -ne $proxy) { + if ($ProxyUseDefaultCredential) { + # Weird hack, $web_request.Proxy returns an IWebProxy object which only guarantees the Credentials + # property. We cannot set UseDefaultCredentials so we just set the Credentials to the + # DefaultCredentials in the CredentialCache which does the same thing. + $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials + } + elseif ($ProxyUsername) { + $proxy.Credentials = New-Object -TypeName System.Net.NetworkCredential -ArgumentList @( + $ProxyUsername, $ProxyPassword + ) + } + else { + $proxy.Credentials = $null + } + } + + $web_request.Proxy = $proxy + + # Some parameters only apply when dealing with a HttpWebRequest + if ($web_request -is [System.Net.HttpWebRequest]) { + if ($Headers) { + foreach ($header in $Headers.GetEnumerator()) { + switch ($header.Key) { + Accept { $web_request.Accept = $header.Value } + Connection { $web_request.Connection = $header.Value } + Content-Length { $web_request.ContentLength = $header.Value } + Content-Type { $web_request.ContentType = $header.Value } + Expect { $web_request.Expect = $header.Value } + Date { $web_request.Date = $header.Value } + Host { $web_request.Host = $header.Value } + If-Modified-Since { $web_request.IfModifiedSince = $header.Value } + Range { $web_request.AddRange($header.Value) } + Referer { $web_request.Referer = $header.Value } + Transfer-Encoding { + $web_request.SendChunked = $true + $web_request.TransferEncoding = $header.Value + } + User-Agent { continue } + default { $web_request.Headers.Add($header.Key, $header.Value) } + } + } + } + + # For backwards compatibility we need to support setting the User-Agent if the header was set in the task. + # We just need to make sure that if an explicit http_agent module was set then that takes priority. + if ($Headers -and $Headers.ContainsKey("User-Agent")) { + if ($HttpAgent -eq $ansible_web_request_options.http_agent.default) { + $HttpAgent = $Headers['User-Agent'] + } + elseif ($null -ne $Module) { + $Module.Warn("The 'User-Agent' header and the 'http_agent' was set, using the 'http_agent' for web request") + } + } + $web_request.UserAgent = $HttpAgent + + switch ($FollowRedirects) { + none { $web_request.AllowAutoRedirect = $false } + safe { + if ($web_request.Method -in @("GET", "HEAD")) { + $web_request.AllowAutoRedirect = $true + } + else { + $web_request.AllowAutoRedirect = $false + } + } + all { $web_request.AllowAutoRedirect = $true } + } + + if ($MaximumRedirection -eq 0) { + $web_request.AllowAutoRedirect = $false + } + else { + $web_request.MaximumAutomaticRedirections = $MaximumRedirection + } + } + + return $web_request +} + +Function Invoke-WithWebRequest { + <# + .SYNOPSIS + Invokes a ScriptBlock with the WebRequest. + + .DESCRIPTION + Invokes the ScriptBlock and handle extra information like accessing the response stream, closing those streams + safely as well as setting common module return values. + + .PARAMETER Module + The Ansible.Basic module to set the return values for. This will set the following return values; + elapsed - The total time, in seconds, that it took to send the web request and process the response + msg - The human readable description of the response status code + status_code - An int that is the response status code + + .PARAMETER Request + The System.Net.WebRequest to call. This can either be manually crafted or created with Get-AnsibleWebRequest. + + .PARAMETER Script + The ScriptBlock to invoke during the web request. This ScriptBlock should take in the params + Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream) + + This scriptblock should manage the response based on what it need to do. + + .PARAMETER Body + An optional Stream to send to the target during the request. + + .PARAMETER IgnoreBadResponse + By default a WebException will be raised for a non 2xx status code and the Script will not be invoked. This + parameter can be set to process all responses regardless of the status code. + + .EXAMPLE Basic module that downloads a file + $spec = @{ + options = @{ + path = @{ type = "path"; required = $true } + } + } + $module = Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-AnsibleWebRequestSpec)) + + $web_request = Get-AnsibleWebRequest -Module $module + + Invoke-WithWebRequest -Module $module -Request $web_request -Script { + Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream) + + $fs = [System.IO.File]::Create($module.Params.path) + try { + $Stream.CopyTo($fs) + $fs.Flush() + } finally { + $fs.Dispose() + } + } + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.Object] + [ValidateScript({ $_.GetType().FullName -eq 'Ansible.Basic.AnsibleModule' })] + $Module, + + [Parameter(Mandatory = $true)] + [System.Net.WebRequest] + $Request, + + [Parameter(Mandatory = $true)] + [ScriptBlock] + $Script, + + [AllowNull()] + [System.IO.Stream] + $Body, + + [Switch] + $IgnoreBadResponse + ) + + $start = Get-Date + if ($null -ne $Body) { + $request_st = $Request.GetRequestStream() + try { + $Body.CopyTo($request_st) + $request_st.Flush() + } + finally { + $request_st.Close() + } + } + + try { + try { + $web_response = $Request.GetResponse() + } + catch [System.Net.WebException] { + # A WebResponse with a status code not in the 200 range will raise a WebException. We check if the + # exception raised contains the actual response and continue on if IgnoreBadResponse is set. We also + # make sure we set the status_code return value on the Module object if possible + + if ($_.Exception.PSObject.Properties.Name -match "Response") { + $web_response = $_.Exception.Response + + if (-not $IgnoreBadResponse -or $null -eq $web_response) { + $Module.Result.msg = $_.Exception.StatusDescription + $Module.Result.status_code = $_.Exception.Response.StatusCode + throw $_ + } + } + else { + throw $_ + } + } + + if ($Request.RequestUri.IsFile) { + # A FileWebResponse won't have these properties set + $Module.Result.msg = "OK" + $Module.Result.status_code = 200 + } + else { + $Module.Result.msg = $web_response.StatusDescription + $Module.Result.status_code = $web_response.StatusCode + } + + $response_stream = $web_response.GetResponseStream() + try { + # Invoke the ScriptBlock and pass in WebResponse and ResponseStream + &$Script -Response $web_response -Stream $response_stream + } + finally { + $response_stream.Dispose() + } + } + finally { + if ($web_response) { + $web_response.Close() + } + $Module.Result.elapsed = ((Get-date) - $start).TotalSeconds + } +} + +Function Get-AnsibleWebRequestSpec { + <# + .SYNOPSIS + Used by modules to get the argument spec fragment for AnsibleModule. + + .EXAMPLES + $spec = @{ + options = @{} + } + $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-AnsibleWebRequestSpec)) + #> + @{ options = $ansible_web_request_options } +} + +# See lib/ansible/plugins/doc_fragments/url_windows.py +# Kept here for backwards compat as this variable was added in Ansible 2.9. Ultimately this util should be removed +# once the deprecation period has been added. +$ansible_web_request_options = @{ + method = @{ type = "str" } + follow_redirects = @{ type = "str"; choices = @("all", "none", "safe"); default = "safe" } + headers = @{ type = "dict" } + http_agent = @{ type = "str"; default = "ansible-httpget" } + maximum_redirection = @{ type = "int"; default = 50 } + timeout = @{ type = "int"; default = 30 } # Was defaulted to 10 in win_get_url but 30 in win_uri so we use 30 + validate_certs = @{ type = "bool"; default = $true } + + # Credential options + client_cert = @{ type = "str" } + client_cert_password = @{ type = "str"; no_log = $true } + force_basic_auth = @{ type = "bool"; default = $false } + url_username = @{ type = "str" } + url_password = @{ type = "str"; no_log = $true } + use_default_credential = @{ type = "bool"; default = $false } + + # Proxy options + use_proxy = @{ type = "bool"; default = $true } + proxy_url = @{ type = "str" } + proxy_username = @{ type = "str" } + proxy_password = @{ type = "str"; no_log = $true } + proxy_use_default_credential = @{ type = "bool"; default = $false } +} + +$export_members = @{ + Function = "Get-AnsibleWebRequest", "Get-AnsibleWebRequestSpec", "Invoke-WithWebRequest" + Variable = "ansible_web_request_options" +} +Export-ModuleMember @export_members diff --git a/lib/ansible/module_utils/powershell/__init__.py b/lib/ansible/module_utils/powershell/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/lib/ansible/module_utils/powershell/__init__.py |