mirror of https://github.com/ansible/ansible.git
win_exec: refactor PS exec runner (#45334)
* win_exec: refactor PS exec runner * more changes for PSCore compatibility * made some changes based on the recent review * split up module exec scripts for smaller payload * removed C# module support to focus on just error msg improvement * cleaned up c# test classifier codepull/46424/head
parent
aa2f3edb49
commit
e972287c35
@ -0,0 +1,2 @@
|
||||
minor_changes:
|
||||
- include better error handling for Windows errors to help with debugging module errors
|
@ -0,0 +1,110 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
|
||||
)
|
||||
|
||||
# help with debugging errors as we don't have visibility of this running process
|
||||
trap {
|
||||
$watchdog_path = "$($env:TEMP)\ansible-async-watchdog-error-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ").txt"
|
||||
$error_msg = "Error while running the async exec wrapper`r`n$(Format-AnsibleException -ErrorRecord $_)"
|
||||
Set-Content -Path $watchdog_path -Value $error_msg
|
||||
break
|
||||
}
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-AnsibleLog "INFO - starting async_watchdog" "async_watchdog"
|
||||
|
||||
# pop 0th action as entrypoint
|
||||
$payload.actions = $payload.actions[1..99]
|
||||
|
||||
$actions = $Payload.actions
|
||||
$entrypoint = $payload.($actions[0])
|
||||
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint))
|
||||
|
||||
$resultfile_path = $payload.async_results_path
|
||||
$max_exec_time_sec = $payload.async_timeout_sec
|
||||
|
||||
Write-AnsibleLog "INFO - deserializing existing result file args at: '$resultfile_path'" "async_watchdog"
|
||||
if (-not (Test-Path -Path $resultfile_path)) {
|
||||
$msg = "result file at '$resultfile_path' does not exist"
|
||||
Write-AnsibleLog "ERROR - $msg" "async_watchdog"
|
||||
throw $msg
|
||||
}
|
||||
$result_json = Get-Content -Path $resultfile_path -Raw
|
||||
Write-AnsibleLog "INFO - result file json is: $result_json" "async_watchdog"
|
||||
$result = ConvertFrom-AnsibleJson -InputObject $result_json
|
||||
|
||||
Write-AnsibleLog "INFO - creating async runspace" "async_watchdog"
|
||||
$rs = [RunspaceFactory]::CreateRunspace()
|
||||
$rs.Open()
|
||||
|
||||
Write-AnsibleLog "INFO - creating async PowerShell pipeline" "async_watchdog"
|
||||
$ps = [PowerShell]::Create()
|
||||
$ps.Runspace = $rs
|
||||
|
||||
# these functions are set in exec_wrapper
|
||||
Write-AnsibleLog "INFO - adding global functions to PowerShell pipeline script" "async_watchdog"
|
||||
$ps.AddScript($script:common_functions).AddStatement() > $null
|
||||
$ps.AddScript($script:wrapper_functions).AddStatement() > $null
|
||||
$ps.AddCommand("Set-Variable").AddParameters(@{Name="common_functions"; Value=$script:common_functions; Scope="script"}).AddStatement() > $null
|
||||
|
||||
Write-AnsibleLog "INFO - adding $($actions[0]) to PowerShell pipeline script" "async_watchdog"
|
||||
$ps.AddScript($entrypoint).AddArgument($payload) > $null
|
||||
|
||||
Write-AnsibleLog "INFO - async job start, calling BeginInvoke()" "async_watchdog"
|
||||
$job_async_result = $ps.BeginInvoke()
|
||||
|
||||
Write-AnsibleLog "INFO - waiting '$max_exec_time_sec' seconds for async job to complete" "async_watchdog"
|
||||
$job_async_result.AsyncWaitHandle.WaitOne($max_exec_time_sec * 1000) > $null
|
||||
$result.finished = 1
|
||||
|
||||
if ($job_async_result.IsCompleted) {
|
||||
Write-AnsibleLog "INFO - async job completed, calling EndInvoke()" "async_watchdog"
|
||||
|
||||
$job_output = $ps.EndInvoke($job_async_result)
|
||||
$job_error = $ps.Streams.Error
|
||||
|
||||
Write-AnsibleLog "INFO - raw module stdout:`r`n$($job_output | Out-String)" "async_watchdog"
|
||||
if ($job_error) {
|
||||
Write-AnsibleLog "WARN - raw module stderr:`r`n$($job_error | Out-String)" "async_watchdog"
|
||||
}
|
||||
|
||||
# write success/output/error to result object
|
||||
# TODO: cleanse leading/trailing junk
|
||||
try {
|
||||
Write-AnsibleLog "INFO - deserializing Ansible stdout" "async_watchdog"
|
||||
$module_result = ConvertFrom-AnsibleJson -InputObject $job_output
|
||||
# TODO: check for conflicting keys
|
||||
$result = $result + $module_result
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "failed to parse module output: $($_.Exception.Message)"
|
||||
# return output back to Ansible to help with debugging errors
|
||||
$result.stdout = $job_output | Out-String
|
||||
$result.stderr = $job_error | Out-String
|
||||
}
|
||||
|
||||
$result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
|
||||
Set-Content -Path $resultfile_path -Value $result_json
|
||||
|
||||
Write-AnsibleLog "INFO - wrote output to $resultfile_path" "async_watchdog"
|
||||
} else {
|
||||
Write-AnsibleLog "ERROR - reached timeout on async job, stopping job" "async_watchdog"
|
||||
$ps.BeginStop($null, $null) > $null # best effort stop
|
||||
|
||||
# write timeout to result object
|
||||
$result.failed = $true
|
||||
$result.msg = "timed out waiting for module completion"
|
||||
$result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
|
||||
Set-Content -Path $resultfile_path -Value $result_json
|
||||
|
||||
Write-AnsibleLog "INFO - wrote timeout to '$resultfile_path'" "async_watchdog"
|
||||
}
|
||||
|
||||
# in the case of a hung pipeline, this will cause the process to stay alive until it's un-hung...
|
||||
#$rs.Close() | Out-Null
|
||||
|
||||
Write-AnsibleLog "INFO - ending async_watchdog" "async_watchdog"
|
@ -0,0 +1,163 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-AnsibleLog "INFO - starting async_wrapper" "async_wrapper"
|
||||
|
||||
if (-not $Payload.environment.ContainsKey("ANSIBLE_ASYNC_DIR")) {
|
||||
Write-AnsibleError -Message "internal error: the environment variable ANSIBLE_ASYNC_DIR is not set and is required for an async task"
|
||||
$host.SetShouldExit(1)
|
||||
return
|
||||
}
|
||||
$async_dir = [System.Environment]::ExpandEnvironmentVariables($Payload.environment.ANSIBLE_ASYNC_DIR)
|
||||
|
||||
# calculate the result path so we can include it in the worker payload
|
||||
$jid = $Payload.async_jid
|
||||
$local_jid = $jid + "." + $pid
|
||||
|
||||
$results_path = [System.IO.Path]::Combine($async_dir, $local_jid)
|
||||
|
||||
Write-AnsibleLog "INFO - creating async results path at '$results_path'" "async_wrapper"
|
||||
|
||||
$Payload.async_results_path = $results_path
|
||||
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) > $null
|
||||
|
||||
# we use Win32_Process to escape the current process job, CreateProcess with a
|
||||
# breakaway flag won't work for psrp as the psrp process does not have breakaway
|
||||
# rights. Unfortunately we can't read/write to the spawned process as we can't
|
||||
# inherit the handles. We use a locked down named pipe to send the exec_wrapper
|
||||
# payload. Anonymous pipes won't work as the spawned process will not be a child
|
||||
# of the current one and will not be able to inherit the handles
|
||||
|
||||
# pop the async_wrapper action so we don't get stuck in a loop and create new
|
||||
# exec_wrapper for our async process
|
||||
$Payload.actions = $Payload.actions[1..99]
|
||||
$payload_json = ConvertTo-Json -InputObject $Payload -Depth 99 -Compress
|
||||
|
||||
$exec_wrapper = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.exec_wrapper))
|
||||
$exec_wrapper = $exec_wrapper.Replace("`$json_raw = ''", "`$json_raw = @'`r`n$payload_json`r`n'@")
|
||||
$payload_bytes = [System.Text.Encoding]::UTF8.GetBytes($exec_wrapper)
|
||||
$pipe_name = "ansible-async-$jid-$([guid]::NewGuid())"
|
||||
|
||||
# template the async process command line with the payload details
|
||||
$bootstrap_wrapper = {
|
||||
# help with debugging errors as we loose visibility of the process output
|
||||
# from here on
|
||||
trap {
|
||||
$wrapper_path = "$($env:TEMP)\ansible-async-wrapper-error-$(Get-Date -Format "yyyy-MM-ddTHH-mm-ss.ffffZ").txt"
|
||||
$error_msg = "Error while running the async exec wrapper`r`n$($_ | Out-String)`r`n$($_.ScriptStackTrace)"
|
||||
Set-Content -Path $wrapper_path -Value $error_msg
|
||||
break
|
||||
}
|
||||
|
||||
&chcp.com 65001 > $null
|
||||
|
||||
# store the pipe name and no. of bytes to read, these are populated before
|
||||
# before the process is created - do not remove or changed
|
||||
$pipe_name = ""
|
||||
$bytes_length = 0
|
||||
|
||||
$input_bytes = New-Object -TypeName byte[] -ArgumentList $bytes_length
|
||||
$pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @(
|
||||
".", # localhost
|
||||
$pipe_name,
|
||||
[System.IO.Pipes.PipeDirection]::In,
|
||||
[System.IO.Pipes.PipeOptions]::None,
|
||||
[System.Security.Principal.TokenImpersonationLevel]::Anonymous
|
||||
)
|
||||
try {
|
||||
$pipe.Connect()
|
||||
$pipe.Read($input_bytes, 0, $bytes_length) > $null
|
||||
} finally {
|
||||
$pipe.Close()
|
||||
}
|
||||
$exec = [System.Text.Encoding]::UTF8.GetString($input_bytes)
|
||||
$exec = [ScriptBlock]::Create($exec)
|
||||
&$exec
|
||||
}
|
||||
|
||||
$bootstrap_wrapper = $bootstrap_wrapper.ToString().Replace('$pipe_name = ""', "`$pipe_name = `"$pipe_name`"")
|
||||
$bootstrap_wrapper = $bootstrap_wrapper.Replace('$bytes_length = 0', "`$bytes_length = $($payload_bytes.Count)")
|
||||
$encoded_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper))
|
||||
$exec_args = "powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command"
|
||||
|
||||
# create a named pipe that is set to allow only the current user read access
|
||||
$current_user = ([Security.Principal.WindowsIdentity]::GetCurrent()).User
|
||||
$pipe_sec = New-Object -TypeName System.IO.Pipes.PipeSecurity
|
||||
$pipe_ar = New-Object -TypeName System.IO.Pipes.PipeAccessRule -ArgumentList @(
|
||||
$current_user,
|
||||
[System.IO.Pipes.PipeAccessRights]::Read,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
)
|
||||
$pipe_sec.AddAccessRule($pipe_ar)
|
||||
|
||||
Write-AnsibleLog "INFO - creating named pipe '$pipe_name'" "async_wrapper"
|
||||
$pipe = New-Object -TypeName System.IO.Pipes.NamedPipeServerStream -ArgumentList @(
|
||||
$pipe_name,
|
||||
[System.IO.Pipes.PipeDirection]::Out,
|
||||
1,
|
||||
[System.IO.Pipes.PipeTransmissionMode]::Byte,
|
||||
[System.IO.Pipes.PipeOptions]::Asynchronous,
|
||||
0,
|
||||
0,
|
||||
$pipe_sec
|
||||
)
|
||||
|
||||
try {
|
||||
Write-AnsibleLog "INFO - creating async process '$exec_args'" "async_wrapper"
|
||||
$process = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{CommandLine=$exec_args}
|
||||
$rc = $process.ReturnValue
|
||||
|
||||
Write-AnsibleLog "INFO - return value from async process exec: $rc" "async_wrapper"
|
||||
if ($rc -ne 0) {
|
||||
$error_msg = switch($rc) {
|
||||
2 { "Access denied" }
|
||||
3 { "Insufficient privilege" }
|
||||
8 { "Unknown failure" }
|
||||
9 { "Path not found" }
|
||||
21 { "Invalid parameter" }
|
||||
default { "Other" }
|
||||
}
|
||||
throw "Failed to start async process: $rc ($error_msg)"
|
||||
}
|
||||
$watchdog_pid = $process.ProcessId
|
||||
Write-AnsibleLog "INFO - created async process PID: $watchdog_pid" "async_wrapper"
|
||||
|
||||
# populate initial results before we send the async data to avoid result race
|
||||
$result = @{
|
||||
started = 1;
|
||||
finished = 0;
|
||||
results_file = $results_path;
|
||||
ansible_job_id = $local_jid;
|
||||
_ansible_suppress_tmpdir_delete = $true;
|
||||
ansible_async_watchdog_pid = $watchdog_pid
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - writing initial async results to '$results_path'" "async_wrapper"
|
||||
$result_json = ConvertTo-Json -InputObject $result -Depth 99 -Compress
|
||||
Set-Content $results_path -Value $result_json
|
||||
|
||||
Write-AnsibleLog "INFO - waiting for async process to connect to named pipe for 5 seconds" "async_wrapper"
|
||||
$wait_async = $pipe.BeginWaitForConnection($null, $null)
|
||||
$wait_async.AsyncWaitHandle.WaitOne(5000) > $null
|
||||
if (-not $wait_async.IsCompleted) {
|
||||
throw "timeout while waiting for child process to connect to named pipe"
|
||||
}
|
||||
$pipe.EndWaitForConnection($wait_async)
|
||||
|
||||
Write-AnsibleLog "INFO - writing exec_wrapper and payload to async process" "async_wrapper"
|
||||
$pipe.Write($payload_bytes, 0, $payload_bytes.Count)
|
||||
$pipe.Flush()
|
||||
$pipe.WaitForPipeDrain()
|
||||
} finally {
|
||||
$pipe.Close()
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - outputting initial async result: $result_json" "async_wrapper"
|
||||
Write-Output -InputObject $result_json
|
||||
Write-AnsibleLog "INFO - ending async_wrapper" "async_wrapper"
|
@ -0,0 +1,142 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
|
||||
)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Become
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-AnsibleLog "INFO - starting become_wrapper" "become_wrapper"
|
||||
|
||||
Function Get-EnumValue($enum, $flag_type, $value, $prefix) {
|
||||
$raw_enum_value = "$prefix$($value.ToUpper())"
|
||||
try {
|
||||
$enum_value = [Enum]::Parse($enum, $raw_enum_value)
|
||||
} catch [System.ArgumentException] {
|
||||
$valid_options = [Enum]::GetNames($enum) | ForEach-Object { $_.Substring($prefix.Length).ToLower() }
|
||||
throw "become_flags $flag_type value '$value' is not valid, valid values are: $($valid_options -join ", ")"
|
||||
}
|
||||
return $enum_value
|
||||
}
|
||||
|
||||
Function Get-BecomeFlags($flags) {
|
||||
$logon_type = [Ansible.Become.LogonType]::LOGON32_LOGON_INTERACTIVE
|
||||
$logon_flags = [Ansible.Become.LogonFlags]::LOGON_WITH_PROFILE
|
||||
|
||||
if ($flags -eq $null -or $flags -eq "") {
|
||||
$flag_split = @()
|
||||
} elseif ($flags -is [string]) {
|
||||
$flag_split = $flags.Split(" ")
|
||||
} else {
|
||||
throw "become_flags must be a string, was $($flags.GetType())"
|
||||
}
|
||||
|
||||
foreach ($flag in $flag_split) {
|
||||
$split = $flag.Split("=")
|
||||
if ($split.Count -ne 2) {
|
||||
throw "become_flags entry '$flag' is in an invalid format, must be a key=value pair"
|
||||
}
|
||||
$flag_key = $split[0]
|
||||
$flag_value = $split[1]
|
||||
if ($flag_key -eq "logon_type") {
|
||||
$enum_details = @{
|
||||
enum = [Ansible.Become.LogonType]
|
||||
flag_type = $flag_key
|
||||
value = $flag_value
|
||||
prefix = "LOGON32_LOGON_"
|
||||
}
|
||||
$logon_type = Get-EnumValue @enum_details
|
||||
} elseif ($flag_key -eq "logon_flags") {
|
||||
$logon_flag_values = $flag_value.Split(",")
|
||||
$logon_flags = 0 -as [Ansible.Become.LogonFlags]
|
||||
foreach ($logon_flag_value in $logon_flag_values) {
|
||||
if ($logon_flag_value -eq "") {
|
||||
continue
|
||||
}
|
||||
$enum_details = @{
|
||||
enum = [Ansible.Become.LogonFlags]
|
||||
flag_type = $flag_key
|
||||
value = $logon_flag_value
|
||||
prefix = "LOGON_"
|
||||
}
|
||||
$logon_flag = Get-EnumValue @enum_details
|
||||
$logon_flags = $logon_flags -bor $logon_flag
|
||||
}
|
||||
} else {
|
||||
throw "become_flags key '$flag_key' is not a valid runas flag, must be 'logon_type' or 'logon_flags'"
|
||||
}
|
||||
}
|
||||
|
||||
return $logon_type, [Ansible.Become.LogonFlags]$logon_flags
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - loading C# become code" "become_wrapper"
|
||||
$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"]))
|
||||
|
||||
# set the TMP env var to _ansible_remote_tmp to ensure the tmp binaries are
|
||||
# compiled to that location
|
||||
$new_tmp = [System.Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"])
|
||||
$old_tmp = $env:TMP
|
||||
$env:TMP = $new_tmp
|
||||
Add-Type -TypeDefinition $become_def -Debug:$false
|
||||
$env:TMP = $old_tmp
|
||||
|
||||
$username = $Payload.become_user
|
||||
$password = $Payload.become_password
|
||||
try {
|
||||
$logon_type, $logon_flags = Get-BecomeFlags -flags $Payload.become_flags
|
||||
} catch {
|
||||
Write-AnsibleError -Message "internal error: failed to parse become_flags '$($Payload.become_flags)'" -ErrorRecord $_
|
||||
$host.SetShouldExit(1)
|
||||
return
|
||||
}
|
||||
Write-AnsibleLog "INFO - parsed become input, user: '$username', type: '$logon_type', flags: '$logon_flags'" "become_wrapper"
|
||||
|
||||
# NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must
|
||||
# bootstrap via small wrapper which contains the exec_wrapper passed through the
|
||||
# stdin pipe. Cannot use 'powershell -' as the $ErrorActionPreference is always
|
||||
# set to Stop and cannot be changed
|
||||
$bootstrap_wrapper = {
|
||||
&chcp.com 65001 > $null
|
||||
$exec_wrapper_str = [System.Console]::In.ReadToEnd()
|
||||
$exec_wrapper = [ScriptBlock]::Create($exec_wrapper_str)
|
||||
&$exec_wrapper
|
||||
}
|
||||
$exec_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper.ToString()))
|
||||
$lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command")
|
||||
$lp_current_directory = $env:SystemRoot # TODO: should this be set to the become user's profile dir?
|
||||
|
||||
# pop the become_wrapper action so we don't get stuck in a loop
|
||||
$Payload.actions = $Payload.actions[1..99]
|
||||
# we want the output from the exec_wrapper to be base64 encoded to preserve unicode chars
|
||||
$Payload.encoded_output = $true
|
||||
|
||||
$payload_json = ConvertTo-Json -InputObject $Payload -Depth 99 -Compress
|
||||
$exec_wrapper = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.exec_wrapper))
|
||||
$exec_wrapper = $exec_wrapper.Replace("`$json_raw = ''", "`$json_raw = @'`r`n$payload_json`r`n'@")
|
||||
|
||||
try {
|
||||
Write-AnsibleLog "INFO - starting become process '$lp_command_line'" "become_wrapper"
|
||||
$result = [Ansible.Become.BecomeUtil]::RunAsUser($username, $password, $lp_command_line,
|
||||
$lp_current_directory, $exec_wrapper, $logon_flags, $logon_type)
|
||||
Write-AnsibleLog "INFO - become process complete with rc: $($result.ExitCode)" "become_wrapper"
|
||||
$stdout = $result.StandardOut
|
||||
try {
|
||||
$stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout))
|
||||
} catch [FormatException] {
|
||||
# output wasn't Base64, ignore as it may contain an error message we want to pass to Ansible
|
||||
Write-AnsibleLog "WARN - become process stdout was not base64 encoded as expected: $stdout"
|
||||
}
|
||||
|
||||
$host.UI.WriteLine($stdout)
|
||||
$host.UI.WriteErrorLine($result.StandardError.Trim())
|
||||
$host.SetShouldExit($result.ExitCode)
|
||||
} catch {
|
||||
Write-AnsibleError -Message "internal error: failed to become user '$username'" -ErrorRecord $_
|
||||
$host.SetShouldExit(1)
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - ending become_wrapper" "become_wrapper"
|
@ -0,0 +1,228 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
begin {
|
||||
$DebugPreference = "Continue"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version 2
|
||||
|
||||
# common functions that are loaded in exec and module context, this is set
|
||||
# as a script scoped variable so async_watchdog and module_wrapper can
|
||||
# access the functions when creating their Runspaces
|
||||
$script:common_functions = {
|
||||
Function ConvertFrom-AnsibleJson {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Converts a JSON string to a Hashtable/Array in the fastest way
|
||||
possible. Unfortunately ConvertFrom-Json is still faster but outputs
|
||||
a PSCustomObject which is combersone for module consumption.
|
||||
|
||||
.PARAMETER InputObject
|
||||
[String] The JSON string to deserialize.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)][String]$InputObject
|
||||
)
|
||||
|
||||
# we can use -AsHashtable to get PowerShell to convert the JSON to
|
||||
# a Hashtable and not a PSCustomObject. This was added in PowerShell
|
||||
# 6.0, fall back to a manual conversion for older versions
|
||||
$cmdlet = Get-Command -Name ConvertFrom-Json -CommandType Cmdlet
|
||||
if ("AsHashtable" -in $cmdlet.Parameters.Keys) {
|
||||
return ,(ConvertFrom-Json -InputObject $InputObject -AsHashtable)
|
||||
} else {
|
||||
# get the PSCustomObject and then manually convert from there
|
||||
$raw_obj = ConvertFrom-Json -InputObject $InputObject
|
||||
|
||||
Function ConvertTo-Hashtable {
|
||||
param($InputObject)
|
||||
|
||||
if ($null -eq $InputObject) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($InputObject -is [PSCustomObject]) {
|
||||
$new_value = @{}
|
||||
foreach ($prop in $InputObject.PSObject.Properties.GetEnumerator()) {
|
||||
$new_value.($prop.Name) = (ConvertTo-Hashtable -InputObject $prop.Value)
|
||||
}
|
||||
return ,$new_value
|
||||
} elseif ($InputObject -is [Array]) {
|
||||
$new_value = [System.Collections.ArrayList]@()
|
||||
foreach ($val in $InputObject) {
|
||||
$new_value.Add((ConvertTo-Hashtable -InputObject $val)) > $null
|
||||
}
|
||||
return ,$new_value.ToArray()
|
||||
} else {
|
||||
return ,$InputObject
|
||||
}
|
||||
}
|
||||
return ,(ConvertTo-Hashtable -InputObject $raw_obj)
|
||||
}
|
||||
}
|
||||
|
||||
Function Format-AnsibleException {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Formats a PowerShell ErrorRecord to a string that's fit for human
|
||||
consumption.
|
||||
|
||||
.NOTES
|
||||
Using Out-String can give us the first part of the exception but it
|
||||
also wraps the messages at 80 chars which is not ideal. We also
|
||||
append the ScriptStackTrace and the .NET StackTrace if present.
|
||||
#>
|
||||
param([System.Management.Automation.ErrorRecord]$ErrorRecord)
|
||||
|
||||
$exception = @"
|
||||
$($ErrorRecord.ToString())
|
||||
$($ErrorRecord.InvocationInfo.PositionMessage)
|
||||
+ CategoryInfo : $($ErrorRecord.CategoryInfo.ToString())
|
||||
+ FullyQualifiedErrorId : $($ErrorRecord.FullyQualifiedErrorId.ToString())
|
||||
"@
|
||||
# module_common strip comments and empty newlines, need to manually
|
||||
# add a preceding newline using `r`n
|
||||
$exception += "`r`n`r`nScriptStackTrace:`r`n$($ErrorRecord.ScriptStackTrace)`r`n"
|
||||
|
||||
# exceptions from C# will also have a StackTrace which we
|
||||
# append if found
|
||||
if ($null -ne $ErrorRecord.Exception.StackTrace) {
|
||||
$exception += "`r`n$($ErrorRecord.Exception.ToString())"
|
||||
}
|
||||
|
||||
return $exception
|
||||
}
|
||||
}
|
||||
.$common_functions
|
||||
|
||||
# common wrapper functions used in the exec wrappers, this is defined in a
|
||||
# script scoped variable so async_watchdog can pass them into the async job
|
||||
$script:wrapper_functions = {
|
||||
Function Write-AnsibleError {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Writes an error message to a JSON string in the format that Ansible
|
||||
understands. Also optionally adds an exception record if the
|
||||
ErrorRecord is passed through.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][String]$Message,
|
||||
[System.Management.Automation.ErrorRecord]$ErrorRecord = $null
|
||||
)
|
||||
$result = @{
|
||||
msg = $Message
|
||||
failed = $true
|
||||
}
|
||||
if ($null -ne $ErrorRecord) {
|
||||
$result.msg += ": $($ErrorRecord.Exception.Message)"
|
||||
$result.exception = (Format-AnsibleException -ErrorRecord $ErrorRecord)
|
||||
}
|
||||
Write-Output -InputObject (ConvertTo-Json -InputObject $result -Depth 99 -Compress)
|
||||
}
|
||||
|
||||
Function Write-AnsibleLog {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Used as a debugging tool to log events to a file as they run in the
|
||||
exec wrappers. By default this is a noop function but the $log_path
|
||||
can be manually set to enable it. Manually set ANSIBLE_EXEC_DEBUG as
|
||||
an env value on the Windows host that this is run on to enable.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)][String]$Message,
|
||||
[Parameter(Position=1)][String]$Wrapper
|
||||
)
|
||||
|
||||
$log_path = $env:ANSIBLE_EXEC_DEBUG
|
||||
if ($log_path) {
|
||||
$log_path = [System.Environment]::ExpandEnvironmentVariables($log_path)
|
||||
$parent_path = [System.IO.Path]::GetDirectoryName($log_path)
|
||||
if (Test-Path -LiteralPath $parent_path -PathType Container) {
|
||||
$msg = "{0:u} - {1} - {2} - " -f (Get-Date), $pid, ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)
|
||||
if ($null -ne $Wrapper) {
|
||||
$msg += "$Wrapper - "
|
||||
}
|
||||
$msg += $Message + "`r`n"
|
||||
$msg_bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)
|
||||
|
||||
$fs = [System.IO.File]::Open($log_path, [System.IO.FileMode]::Append,
|
||||
[System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
$fs.Write($msg_bytes, 0, $msg_bytes.Length)
|
||||
} finally {
|
||||
$fs.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.$wrapper_functions
|
||||
|
||||
# NB: do not adjust the following line - it is replaced when doing
|
||||
# non-streamed input
|
||||
$json_raw = ''
|
||||
} process {
|
||||
$json_raw += [String]$input
|
||||
} end {
|
||||
Write-AnsibleLog "INFO - starting exec_wrapper" "exec_wrapper"
|
||||
if (-not $json_raw) {
|
||||
Write-AnsibleError -Message "internal error: no input given to PowerShell exec wrapper"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - converting json raw to a payload" "exec_wrapper"
|
||||
$payload = ConvertFrom-AnsibleJson -InputObject $json_raw
|
||||
|
||||
# TODO: handle binary modules
|
||||
# TODO: handle persistence
|
||||
|
||||
if ($payload.min_os_version) {
|
||||
$min_os_version = [Version]$payload.min_os_version
|
||||
# Environment.OSVersion.Version is deprecated and may not return the
|
||||
# right version
|
||||
$actual_os_version = [Version](Get-Item -Path $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion
|
||||
|
||||
Write-AnsibleLog "INFO - checking if actual os version '$actual_os_version' is less than the min os version '$min_os_version'" "exec_wrapper"
|
||||
if ($actual_os_version -lt $min_os_version) {
|
||||
Write-AnsibleError -Message "internal error: This module cannot run on this OS as it requires a minimum version of $min_os_version, actual was $actual_os_version"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
if ($payload.min_ps_version) {
|
||||
$min_ps_version = [Version]$payload.min_ps_version
|
||||
$actual_ps_version = $PSVersionTable.PSVersion
|
||||
|
||||
Write-AnsibleLog "INFO - checking if actual PS version '$actual_ps_version' is less than the min PS version '$min_ps_version'" "exec_wrapper"
|
||||
if ($actual_ps_version -lt $min_ps_version) {
|
||||
Write-AnsibleError -Message "internal error: This module cannot run as it requires a minimum PowerShell version of $min_ps_version, actual was $actual_ps_version"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# pop 0th action as entrypoint
|
||||
$action = $payload.actions[0]
|
||||
Write-AnsibleLog "INFO - running action $action" "exec_wrapper"
|
||||
|
||||
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload.($action)))
|
||||
$entrypoint = [ScriptBlock]::Create($entrypoint)
|
||||
# so we preserve the formatting and don't fall prey to locale issues, some
|
||||
# wrappers want the output to be in base64 form, we store the value here in
|
||||
# case the wrapper changes the value when they create a payload for their
|
||||
# own exec_wrapper
|
||||
$encoded_output = $payload.encoded_output
|
||||
|
||||
try {
|
||||
$output = &$entrypoint -Payload $payload
|
||||
if ($encoded_output -and $null -ne $output) {
|
||||
$b64_output = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($output))
|
||||
Write-Output -InputObject $b64_output
|
||||
} else {
|
||||
Write-Output -InputObject $output
|
||||
}
|
||||
} catch {
|
||||
Write-AnsibleError -Message "internal error: failed to run exec_wrapper action $action" -ErrorRecord $_
|
||||
exit 1
|
||||
}
|
||||
Write-AnsibleLog "INFO - ending exec_wrapper" "exec_wrapper"
|
||||
}
|
@ -0,0 +1,288 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import pkgutil
|
||||
import random
|
||||
import re
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.plugins.loader import ps_module_utils_loader
|
||||
|
||||
|
||||
class PSModuleDepFinder(object):
|
||||
|
||||
def __init__(self):
|
||||
self.ps_modules = dict()
|
||||
self.exec_scripts = dict()
|
||||
|
||||
# by defining an explicit dict of cs utils and where they are used, we
|
||||
# can potentially save time by not adding the type multiple times if it
|
||||
# isn't needed
|
||||
self.cs_utils_wrapper = dict()
|
||||
self.cs_utils_module = dict()
|
||||
|
||||
self.ps_version = None
|
||||
self.os_version = None
|
||||
self.become = False
|
||||
|
||||
self._re_cs_module = re.compile(to_bytes(r'(?i)^using\s(Ansible\..+);$'))
|
||||
self._re_cs_in_ps_module = re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)'))
|
||||
self._re_module = re.compile(to_bytes(r'(?i)^#\s*requires\s+\-module(?:s?)\s*(Ansible\.ModuleUtils\..+)'))
|
||||
self._re_wrapper = re.compile(to_bytes(r'(?i)^#\s*ansiblerequires\s+-wrapper\s+(\w*)'))
|
||||
self._re_ps_version = re.compile(to_bytes(r'(?i)^#requires\s+\-version\s+([0-9]+(\.[0-9]+){0,3})$'))
|
||||
self._re_os_version = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-osversion\s+([0-9]+(\.[0-9]+){0,3})$'))
|
||||
self._re_become = re.compile(to_bytes(r'(?i)^#ansiblerequires\s+\-become$'))
|
||||
|
||||
def scan_module(self, module_data, wrapper=False, powershell=True):
|
||||
lines = module_data.split(b'\n')
|
||||
module_utils = set()
|
||||
if wrapper:
|
||||
cs_utils = self.cs_utils_wrapper
|
||||
else:
|
||||
cs_utils = self.cs_utils_module
|
||||
|
||||
if powershell:
|
||||
checks = [
|
||||
# PS module contains '#Requires -Module Ansible.ModuleUtils.*'
|
||||
(self._re_module, self.ps_modules, ".psm1"),
|
||||
# PS module contains '#AnsibleRequires -CSharpUtil Ansible.*'
|
||||
(self._re_cs_in_ps_module, cs_utils, ".cs"),
|
||||
]
|
||||
else:
|
||||
checks = [
|
||||
# CS module contains 'using Ansible.*;'
|
||||
(self._re_cs_module, cs_utils, ".cs"),
|
||||
]
|
||||
|
||||
for line in lines:
|
||||
for check in checks:
|
||||
match = check[0].match(line)
|
||||
if match:
|
||||
# tolerate windows line endings by stripping any remaining
|
||||
# newline chars
|
||||
module_util_name = to_text(match.group(1).rstrip())
|
||||
if module_util_name not in check[1].keys():
|
||||
module_utils.add((module_util_name, check[2]))
|
||||
|
||||
if powershell:
|
||||
ps_version_match = self._re_ps_version.match(line)
|
||||
if ps_version_match:
|
||||
self._parse_version_match(ps_version_match, "ps_version")
|
||||
|
||||
os_version_match = self._re_os_version.match(line)
|
||||
if os_version_match:
|
||||
self._parse_version_match(os_version_match, "os_version")
|
||||
|
||||
# once become is set, no need to keep on checking recursively
|
||||
if not self.become:
|
||||
become_match = self._re_become.match(line)
|
||||
if become_match:
|
||||
self.become = True
|
||||
|
||||
if wrapper:
|
||||
wrapper_match = self._re_wrapper.match(line)
|
||||
if wrapper_match:
|
||||
self.scan_exec_script(wrapper_match.group(1).rstrip())
|
||||
|
||||
# recursively drill into each Requires to see if there are any more
|
||||
# requirements
|
||||
for m in set(module_utils):
|
||||
self._add_module(m, wrapper=wrapper)
|
||||
|
||||
def scan_exec_script(self, name):
|
||||
# scans lib/ansible/executor/powershell for scripts used in the module
|
||||
# exec side. It also scans these scripts for any dependencies
|
||||
name = to_text(name)
|
||||
if name in self.exec_scripts.keys():
|
||||
return
|
||||
|
||||
data = pkgutil.get_data("ansible.executor.powershell", name + ".ps1")
|
||||
if data is None:
|
||||
raise AnsibleError("Could not find executor powershell script "
|
||||
"for '%s'" % name)
|
||||
|
||||
b_data = to_bytes(data)
|
||||
|
||||
# remove comments to reduce the payload size in the exec wrappers
|
||||
if C.DEFAULT_DEBUG:
|
||||
exec_script = b_data
|
||||
else:
|
||||
exec_script = _strip_comments(b_data)
|
||||
self.exec_scripts[name] = to_bytes(exec_script)
|
||||
self.scan_module(b_data, wrapper=True, powershell=True)
|
||||
|
||||
def _add_module(self, name, wrapper=False):
|
||||
m, ext = name
|
||||
m = to_text(m)
|
||||
mu_path = ps_module_utils_loader.find_plugin(m, ext)
|
||||
if not mu_path:
|
||||
raise AnsibleError('Could not find imported module support code '
|
||||
'for \'%s\'' % m)
|
||||
|
||||
module_util_data = to_bytes(_slurp(mu_path))
|
||||
if ext == ".psm1":
|
||||
self.ps_modules[m] = module_util_data
|
||||
else:
|
||||
if wrapper:
|
||||
self.cs_utils_wrapper[m] = module_util_data
|
||||
else:
|
||||
self.cs_utils_module[m] = module_util_data
|
||||
self.scan_module(module_util_data, wrapper=wrapper,
|
||||
powershell=(ext == ".psm1"))
|
||||
|
||||
def _parse_version_match(self, match, attribute):
|
||||
new_version = to_text(match.group(1)).rstrip()
|
||||
|
||||
# PowerShell cannot cast a string of "1" to Version, it must have at
|
||||
# least the major.minor for it to be valid so we append 0
|
||||
if match.group(2) is None:
|
||||
new_version = "%s.0" % new_version
|
||||
|
||||
existing_version = getattr(self, attribute, None)
|
||||
if existing_version is None:
|
||||
setattr(self, attribute, new_version)
|
||||
else:
|
||||
# determine which is the latest version and set that
|
||||
if LooseVersion(new_version) > LooseVersion(existing_version):
|
||||
setattr(self, attribute, new_version)
|
||||
|
||||
|
||||
def _slurp(path):
|
||||
if not os.path.exists(path):
|
||||
raise AnsibleError("imported module support code does not exist at %s"
|
||||
% os.path.abspath(path))
|
||||
fd = open(path, 'rb')
|
||||
data = fd.read()
|
||||
fd.close()
|
||||
return data
|
||||
|
||||
|
||||
def _strip_comments(source):
|
||||
# Strip comments and blank lines from the wrapper
|
||||
buf = []
|
||||
start_block = False
|
||||
for line in source.splitlines():
|
||||
l = line.strip()
|
||||
|
||||
if start_block and l.endswith(b'#>'):
|
||||
start_block = False
|
||||
continue
|
||||
elif start_block:
|
||||
continue
|
||||
elif l.startswith(b'<#'):
|
||||
start_block = True
|
||||
continue
|
||||
elif not l or l.startswith(b'#'):
|
||||
continue
|
||||
|
||||
buf.append(line)
|
||||
return b'\n'.join(buf)
|
||||
|
||||
|
||||
def _create_powershell_wrapper(b_module_data, module_args, environment,
|
||||
async_timeout, become, become_method,
|
||||
become_user, become_password, become_flags,
|
||||
substyle):
|
||||
# creates the manifest/wrapper used in PowerShell/C# modules to enable
|
||||
# things like become and async - this is also called in action/script.py
|
||||
|
||||
# FUTURE: add process_wrapper.ps1 to run module_wrapper in a new process
|
||||
# if running under a persistent connection and substyle is C# so we
|
||||
# don't have type conflicts
|
||||
finder = PSModuleDepFinder()
|
||||
if substyle != 'script':
|
||||
# don't scan the module for util dependencies and other Ansible related
|
||||
# flags if the substyle is 'script' which is set by action/script
|
||||
finder.scan_module(b_module_data, powershell=(substyle == "powershell"))
|
||||
|
||||
module_wrapper = "module_%s_wrapper" % substyle
|
||||
exec_manifest = dict(
|
||||
module_entry=to_text(base64.b64encode(b_module_data)),
|
||||
powershell_modules=dict(),
|
||||
csharp_utils=dict(),
|
||||
csharp_utils_module=list(), # csharp_utils only required by a module
|
||||
module_args=module_args,
|
||||
actions=[module_wrapper],
|
||||
environment=environment,
|
||||
encoded_output=False
|
||||
)
|
||||
finder.scan_exec_script(module_wrapper)
|
||||
|
||||
if async_timeout > 0:
|
||||
finder.scan_exec_script('exec_wrapper')
|
||||
finder.scan_exec_script('async_watchdog')
|
||||
finder.scan_exec_script('async_wrapper')
|
||||
|
||||
exec_manifest["actions"].insert(0, 'async_watchdog')
|
||||
exec_manifest["actions"].insert(0, 'async_wrapper')
|
||||
exec_manifest["async_jid"] = str(random.randint(0, 999999999999))
|
||||
exec_manifest["async_timeout_sec"] = async_timeout
|
||||
|
||||
if become and become_method == 'runas':
|
||||
finder.scan_exec_script('exec_wrapper')
|
||||
finder.scan_exec_script('become_wrapper')
|
||||
|
||||
exec_manifest["actions"].insert(0, 'become_wrapper')
|
||||
exec_manifest["become_user"] = become_user
|
||||
exec_manifest["become_password"] = become_password
|
||||
exec_manifest['become_flags'] = become_flags
|
||||
|
||||
exec_manifest['min_ps_version'] = finder.ps_version
|
||||
exec_manifest['min_os_version'] = finder.os_version
|
||||
if finder.become and 'become_wrapper' not in exec_manifest['actions']:
|
||||
finder.scan_exec_script('exec_wrapper')
|
||||
finder.scan_exec_script('become_wrapper')
|
||||
|
||||
exec_manifest['actions'].insert(0, 'become_wrapper')
|
||||
exec_manifest['become_user'] = 'SYSTEM'
|
||||
exec_manifest['become_password'] = None
|
||||
exec_manifest['become_flags'] = None
|
||||
|
||||
# make sure Ansible.ModuleUtils.AddType is added if any C# utils are used
|
||||
if len(finder.cs_utils_wrapper) > 0 or len(finder.cs_utils_module) > 0:
|
||||
finder._add_module((b"Ansible.ModuleUtils.AddType", ".psm1"),
|
||||
wrapper=False)
|
||||
|
||||
# exec_wrapper is only required to be part of the payload if using
|
||||
# become or async, to save on payload space we check if exec_wrapper has
|
||||
# already been added, and remove it manually if it hasn't later
|
||||
exec_required = "exec_wrapper" in finder.exec_scripts.keys()
|
||||
finder.scan_exec_script("exec_wrapper")
|
||||
# must contain an empty newline so it runs the begin/process/end block
|
||||
finder.exec_scripts["exec_wrapper"] += b"\n\n"
|
||||
|
||||
exec_wrapper = finder.exec_scripts["exec_wrapper"]
|
||||
if not exec_required:
|
||||
finder.exec_scripts.pop("exec_wrapper")
|
||||
|
||||
for name, data in finder.exec_scripts.items():
|
||||
b64_data = to_text(base64.b64encode(data))
|
||||
exec_manifest[name] = b64_data
|
||||
|
||||
for name, data in finder.ps_modules.items():
|
||||
b64_data = to_text(base64.b64encode(data))
|
||||
exec_manifest['powershell_modules'][name] = b64_data
|
||||
|
||||
cs_utils = finder.cs_utils_wrapper
|
||||
cs_utils.update(finder.cs_utils_module)
|
||||
for name, data in cs_utils.items():
|
||||
b64_data = to_text(base64.b64encode(data))
|
||||
exec_manifest['csharp_utils'][name] = b64_data
|
||||
exec_manifest['csharp_utils_module'] = list(finder.cs_utils_module.keys())
|
||||
|
||||
# FUTURE: smuggle this back as a dict instead of serializing here;
|
||||
# the connection plugin may need to modify it
|
||||
b_json = to_bytes(json.dumps(exec_manifest))
|
||||
b_data = exec_wrapper.replace(b"$json_raw = ''",
|
||||
b"$json_raw = @'\r\n%s\r\n'@" % b_json)
|
||||
return b_data
|
@ -0,0 +1,57 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
|
||||
)
|
||||
|
||||
#AnsibleRequires -Wrapper module_wrapper
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-AnsibleLog "INFO - starting module_powershell_wrapper" "module_powershell_wrapper"
|
||||
|
||||
$module_name = $Payload.module_args["_ansible_module_name"]
|
||||
Write-AnsibleLog "INFO - building module payload for '$module_name'" "module_powershell_wrapper"
|
||||
|
||||
# compile any C# module utils passed in from the controller, Add-CSharpType is
|
||||
# automatically added to the payload manifest if any csharp util is set
|
||||
$csharp_utils = [System.Collections.ArrayList]@()
|
||||
foreach ($csharp_util in $Payload.csharp_utils_module) {
|
||||
Write-AnsibleLog "INFO - adding $csharp_util to list of C# references to compile" "module_powershell_wrapper"
|
||||
$util_code = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils[$csharp_util]))
|
||||
$csharp_utils.Add($util_code) > $null
|
||||
}
|
||||
if ($csharp_utils.Count -gt 0) {
|
||||
$add_type_b64 = $Payload.powershell_modules["Ansible.ModuleUtils.AddType"]
|
||||
$add_type = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($add_type_b64))
|
||||
New-Module -Name Ansible.ModuleUtils.AddType -ScriptBlock ([ScriptBlock]::Create($add_type)) | Import-Module > $null
|
||||
|
||||
# add any C# references so the module does not have to do so
|
||||
$new_tmp = [System.Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"])
|
||||
Add-CSharpType -References $csharp_utils -TempPath $new_tmp -IncludeDebugInfo
|
||||
}
|
||||
|
||||
# get the common module_wrapper code and invoke that to run the module
|
||||
$variables = [System.Collections.ArrayList]@(@{ Name = "complex_args"; Value = $Payload.module_args; Scope = "Global" })
|
||||
$module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.module_entry))
|
||||
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload.module_wrapper))
|
||||
$entrypoint = [ScriptBlock]::Create($entrypoint)
|
||||
|
||||
try {
|
||||
&$entrypoint -Scripts $script:common_functions, $module -Variables $variables `
|
||||
-Environment $Payload.environment -Modules $Payload.powershell_modules `
|
||||
-ModuleName $module_name
|
||||
} catch {
|
||||
# failed to invoke the PowerShell module, capture the exception and
|
||||
# output a pretty error for Ansible to parse
|
||||
$result = @{
|
||||
msg = "Failed to invoke PowerShell module: $($_.Exception.Message)"
|
||||
failed = $true
|
||||
exception = (Format-AnsibleException -ErrorRecord $_)
|
||||
}
|
||||
Write-Output -InputObject (ConvertTo-Json -InputObject $result -Depth 99 -Compress)
|
||||
$host.SetShouldExit(1)
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - ending module_powershell_wrapper" "module_powershell_wrapper"
|
@ -0,0 +1,22 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
|
||||
)
|
||||
|
||||
#AnsibleRequires -Wrapper module_wrapper
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-AnsibleLog "INFO - starting module_script_wrapper" "module_script_wrapper"
|
||||
|
||||
$script = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.module_entry))
|
||||
|
||||
# get the common module_wrapper code and invoke that to run the module
|
||||
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($payload.module_wrapper))
|
||||
$entrypoint = [ScriptBlock]::Create($entrypoint)
|
||||
|
||||
&$entrypoint -Scripts $script -Environment $Payload.environment -ModuleName "script"
|
||||
|
||||
Write-AnsibleLog "INFO - ending module_script_wrapper" "module_script_wrapper"
|
@ -0,0 +1,165 @@
|
||||
# (c) 2018 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Invokes an Ansible module in a new Runspace. This cmdlet will output the
|
||||
module's output and write any errors to the error stream of the current
|
||||
host.
|
||||
|
||||
.PARAMETER Scripts
|
||||
[Object[]] String or ScriptBlocks to execute.
|
||||
|
||||
.PARAMETER Variables
|
||||
[System.Collections.ArrayList] The variables to set in the new Pipeline.
|
||||
Each value is a hashtable that contains the parameters to use with
|
||||
Set-Variable;
|
||||
Name: the name of the variable to set
|
||||
Value: the value of the variable to set
|
||||
Scope: the scope of the variable
|
||||
|
||||
.PARAMETER Environment
|
||||
[System.Collections.IDictionary] A Dictionary of environment key/values to
|
||||
set in the new Pipeline.
|
||||
|
||||
.PARAMETER Modules
|
||||
[System.Collections.IDictionary] A Dictionary of PowerShell modules to
|
||||
import into the new Pipeline. The key is the name of the module and the
|
||||
value is a base64 string of the module util code.
|
||||
|
||||
.PARAMETER ModuleName
|
||||
[String] The name of the module that is being executed.
|
||||
#>
|
||||
param(
|
||||
[Object[]]$Scripts,
|
||||
[System.Collections.ArrayList][AllowEmptyCollection()]$Variables,
|
||||
[System.Collections.IDictionary]$Environment,
|
||||
[System.Collections.IDictionary]$Modules,
|
||||
[String]$ModuleName
|
||||
)
|
||||
|
||||
Write-AnsibleLog "INFO - creating new PowerShell pipeline for $ModuleName" "module_wrapper"
|
||||
$ps = [PowerShell]::Create()
|
||||
|
||||
# do not set ErrorActionPreference for script
|
||||
if ($ModuleName -ne "script") {
|
||||
$ps.Runspace.SessionStateProxy.SetVariable("ErrorActionPreference", "Stop")
|
||||
}
|
||||
|
||||
# force input encoding to preamble-free UTF8 so PS sub-processes (eg,
|
||||
# Start-Job) don't blow up. This is only required for WinRM, a PSRP
|
||||
# runspace doesn't have a host console and this will bomb out
|
||||
if ($host.Name -eq "ConsoleHost") {
|
||||
Write-AnsibleLog "INFO - setting console input encoding to UTF8 for $ModuleName" "module_wrapper"
|
||||
$ps.AddScript('[Console]::InputEncoding = New-Object Text.UTF8Encoding $false').AddStatement() > $null
|
||||
}
|
||||
|
||||
# set the variables
|
||||
foreach ($variable in $Variables) {
|
||||
Write-AnsibleLog "INFO - setting variable '$($variable.Name)' for $ModuleName" "module_wrapper"
|
||||
$ps.AddCommand("Set-Variable").AddParameters($variable).AddStatement() > $null
|
||||
}
|
||||
|
||||
# set the environment vars
|
||||
if ($Environment) {
|
||||
foreach ($env_kv in $Environment.GetEnumerator()) {
|
||||
Write-AnsibleLog "INFO - setting environment '$($env_kv.Key)' for $ModuleName" "module_wrapper"
|
||||
$env_key = $env_kv.Key.Replace("'", "''")
|
||||
$env_value = $env_kv.Value.ToString().Replace("'", "''")
|
||||
$escaped_env_set = "[System.Environment]::SetEnvironmentVariable('$env_key', '$env_value')"
|
||||
$ps.AddScript($escaped_env_set).AddStatement() > $null
|
||||
}
|
||||
}
|
||||
|
||||
# import the PS modules
|
||||
if ($Modules) {
|
||||
foreach ($module in $Modules.GetEnumerator()) {
|
||||
Write-AnsibleLog "INFO - create module util '$($module.Key)' for $ModuleName" "module_wrapper"
|
||||
$module_name = $module.Key
|
||||
$module_code = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($module.Value))
|
||||
$ps.AddCommand("New-Module").AddParameters(@{Name=$module_name; ScriptBlock=[ScriptBlock]::Create($module_code)}) > $null
|
||||
$ps.AddCommand("Import-Module").AddParameter("WarningAction", "SilentlyContinue") > $null
|
||||
$ps.AddCommand("Out-Null").AddStatement() > $null
|
||||
}
|
||||
}
|
||||
|
||||
# redefine Write-Host to dump to output instead of failing
|
||||
# lots of scripts still use it
|
||||
$ps.AddScript('Function Write-Host($msg) { Write-Output -InputObject $msg }').AddStatement() > $null
|
||||
|
||||
# add the scripts and run
|
||||
foreach ($script in $Scripts) {
|
||||
$ps.AddScript($script).AddStatement() > $null
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - start module exec with Invoke() - $ModuleName" "module_wrapper"
|
||||
try {
|
||||
$module_output = $ps.Invoke()
|
||||
} catch {
|
||||
# uncaught exception while executing module, present a prettier error for
|
||||
# Ansible to parse
|
||||
Write-AnsibleError -Message "Unhandled exception while executing module" `
|
||||
-ErrorRecord $_.Exception.InnerException.ErrorRecord
|
||||
$host.SetShouldExit(1)
|
||||
return
|
||||
}
|
||||
|
||||
# other types of errors may not throw an exception in Invoke but rather just
|
||||
# set the pipeline state to failed
|
||||
if ($ps.InvocationStateInfo.State -eq "Failed" -and $ModuleName -ne "script") {
|
||||
Write-AnsibleError -Message "Unhandled exception while executing module" `
|
||||
-ErrorRecord $ps.InvocationStateInfo.Reason.ErrorRecord
|
||||
$host.SetShouldExit(1)
|
||||
return
|
||||
}
|
||||
|
||||
Write-AnsibleLog "INFO - module exec ended $ModuleName" "module_wrapper"
|
||||
$ansible_output = $ps.Runspace.SessionStateProxy.GetVariable("_ansible_output")
|
||||
|
||||
# _ansible_output is a special var used by new modules to store the
|
||||
# output JSON. If set, we consider the ExitJson and FailJson methods
|
||||
# called and assume it contains the JSON we want and the pipeline
|
||||
# output won't contain anything of note
|
||||
# TODO: should we validate it or use a random variable name?
|
||||
# TODO: should we use this behaviour for all new modules and not just
|
||||
# ones running under psrp
|
||||
if ($null -ne $ansible_output) {
|
||||
Write-AnsibleLog "INFO - using the _ansible_output variable for module output - $ModuleName" "module_wrapper"
|
||||
Write-Output -InputObject $ansible_output.ToString()
|
||||
} elseif ($module_output.Count -gt 0) {
|
||||
# do not output if empty collection
|
||||
Write-AnsibleLog "INFO - using the output stream for module output - $ModuleName" "module_wrapper"
|
||||
Write-Output -InputObject ($module_output -join "`r`n")
|
||||
}
|
||||
|
||||
# we attempt to get the return code from the LASTEXITCODE variable
|
||||
# this is set explicitly in newer style variables when calling
|
||||
# ExitJson and FailJson. If set we set the current hosts' exit code
|
||||
# to that same value
|
||||
$rc = $ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE")
|
||||
if ($null -ne $rc) {
|
||||
Write-AnsibleLog "INFO - got an rc of $rc from $ModuleName exec" "module_wrapper"
|
||||
$host.SetShouldExit($rc)
|
||||
}
|
||||
|
||||
# PS3 doesn't properly set HadErrors in many cases, inspect the error stream as a fallback
|
||||
# with the trap handler that's now in place, this should only write to the output if
|
||||
# $ErrorActionPreference != "Stop", that's ok because this is sent to the stderr output
|
||||
# for a user to manually debug if something went horribly wrong
|
||||
if ($ps.HadErrors -or ($PSVersionTable.PSVersion.Major -lt 4 -and $ps.Streams.Error.Count -gt 0)) {
|
||||
Write-AnsibleLog "WARN - module had errors, outputting error info $ModuleName" "module_wrapper"
|
||||
# if the rc wasn't explicitly set, we return an exit code of 1
|
||||
if ($null -eq $rc) {
|
||||
$host.SetShouldExit(1)
|
||||
}
|
||||
|
||||
# output each error to the error stream of the current pipeline
|
||||
foreach ($err in $ps.Streams.Error) {
|
||||
$error_msg = Format-AnsibleException -ErrorRecord $err
|
||||
|
||||
# need to use the current hosts's UI class as we may not have
|
||||
# a console to write the stderr to, e.g. psrp
|
||||
Write-AnsibleLog "WARN - error msg for for $($ModuleName):`r`n$error_msg" "module_wrapper"
|
||||
$host.UI.WriteErrorLine($error_msg)
|
||||
}
|
||||
}
|
@ -0,0 +1,721 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
// TODO: make some classes/structs private/internal before the next release
|
||||
|
||||
namespace Ansible.Become
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class SECURITY_ATTRIBUTES
|
||||
{
|
||||
public int nLength;
|
||||
public IntPtr lpSecurityDescriptor;
|
||||
public bool bInheritHandle = false;
|
||||
public SECURITY_ATTRIBUTES()
|
||||
{
|
||||
nLength = Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFO
|
||||
{
|
||||
public Int32 cb;
|
||||
public IntPtr lpReserved;
|
||||
public IntPtr lpDesktop;
|
||||
public IntPtr lpTitle;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
|
||||
public byte[] _data1;
|
||||
public Int32 dwFlags;
|
||||
public Int16 wShowWindow;
|
||||
public Int16 cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public SafeFileHandle hStdInput;
|
||||
public SafeFileHandle hStdOutput;
|
||||
public SafeFileHandle hStdError;
|
||||
public STARTUPINFO()
|
||||
{
|
||||
cb = Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class STARTUPINFOEX
|
||||
{
|
||||
public STARTUPINFO startupInfo;
|
||||
public IntPtr lpAttributeList;
|
||||
public STARTUPINFOEX()
|
||||
{
|
||||
startupInfo = new STARTUPINFO();
|
||||
startupInfo.cb = Marshal.SizeOf(this);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SID_AND_ATTRIBUTES
|
||||
{
|
||||
public IntPtr Sid;
|
||||
public int Attributes;
|
||||
}
|
||||
|
||||
public struct TOKEN_USER
|
||||
{
|
||||
public SID_AND_ATTRIBUTES User;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum StartupInfoFlags : uint
|
||||
{
|
||||
USESTDHANDLES = 0x00000100
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CreationFlags : uint
|
||||
{
|
||||
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
|
||||
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
|
||||
CREATE_NEW_CONSOLE = 0x00000010,
|
||||
CREATE_SUSPENDED = 0x00000004,
|
||||
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
|
||||
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
|
||||
}
|
||||
|
||||
public enum HandleFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
INHERIT = 1
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum LogonFlags
|
||||
{
|
||||
LOGON_WITH_PROFILE = 0x00000001,
|
||||
LOGON_NETCREDENTIALS_ONLY = 0x00000002
|
||||
}
|
||||
|
||||
public enum LogonType
|
||||
{
|
||||
LOGON32_LOGON_INTERACTIVE = 2,
|
||||
LOGON32_LOGON_NETWORK = 3,
|
||||
LOGON32_LOGON_BATCH = 4,
|
||||
LOGON32_LOGON_SERVICE = 5,
|
||||
LOGON32_LOGON_UNLOCK = 7,
|
||||
LOGON32_LOGON_NETWORK_CLEARTEXT = 8,
|
||||
LOGON32_LOGON_NEW_CREDENTIALS = 9
|
||||
}
|
||||
|
||||
public enum LogonProvider
|
||||
{
|
||||
LOGON32_PROVIDER_DEFAULT = 0,
|
||||
}
|
||||
|
||||
public enum TokenInformationClass
|
||||
{
|
||||
TokenUser = 1,
|
||||
TokenType = 8,
|
||||
TokenImpersonationLevel = 9,
|
||||
TokenElevationType = 18,
|
||||
TokenLinkedToken = 19,
|
||||
}
|
||||
|
||||
public enum TokenElevationType
|
||||
{
|
||||
TokenElevationTypeDefault = 1,
|
||||
TokenElevationTypeFull,
|
||||
TokenElevationTypeLimited
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ProcessAccessFlags : uint
|
||||
{
|
||||
PROCESS_QUERY_INFORMATION = 0x00000400,
|
||||
}
|
||||
|
||||
public enum SECURITY_IMPERSONATION_LEVEL
|
||||
{
|
||||
SecurityImpersonation,
|
||||
}
|
||||
|
||||
public enum TOKEN_TYPE
|
||||
{
|
||||
TokenPrimary = 1,
|
||||
TokenImpersonation
|
||||
}
|
||||
|
||||
class NativeWaitHandle : WaitHandle
|
||||
{
|
||||
public NativeWaitHandle(IntPtr handle)
|
||||
{
|
||||
this.SafeWaitHandle = new SafeWaitHandle(handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
public class Win32Exception : System.ComponentModel.Win32Exception
|
||||
{
|
||||
private string _msg;
|
||||
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||
public Win32Exception(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 Win32Exception(string message) { return new Win32Exception(message); }
|
||||
}
|
||||
|
||||
public class CommandResult
|
||||
{
|
||||
public string StandardOut { get; internal set; }
|
||||
public string StandardError { get; internal set; }
|
||||
public uint ExitCode { get; internal set; }
|
||||
}
|
||||
|
||||
public class BecomeUtil
|
||||
{
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool LogonUser(
|
||||
string lpszUsername,
|
||||
string lpszDomain,
|
||||
string lpszPassword,
|
||||
LogonType dwLogonType,
|
||||
LogonProvider dwLogonProvider,
|
||||
out IntPtr phToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool CreateProcessWithTokenW(
|
||||
IntPtr hToken,
|
||||
LogonFlags dwLogonFlags,
|
||||
[MarshalAs(UnmanagedType.LPTStr)]
|
||||
string lpApplicationName,
|
||||
StringBuilder lpCommandLine,
|
||||
CreationFlags dwCreationFlags,
|
||||
IntPtr lpEnvironment,
|
||||
[MarshalAs(UnmanagedType.LPTStr)]
|
||||
string lpCurrentDirectory,
|
||||
STARTUPINFOEX lpStartupInfo,
|
||||
out PROCESS_INFORMATION lpProcessInformation);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool CreatePipe(
|
||||
out SafeFileHandle hReadPipe,
|
||||
out SafeFileHandle hWritePipe,
|
||||
SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||
uint nSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool SetHandleInformation(
|
||||
SafeFileHandle hObject,
|
||||
HandleFlags dwMask,
|
||||
int dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool GetExitCodeProcess(
|
||||
IntPtr hProcess,
|
||||
out uint lpExitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool CloseHandle(
|
||||
IntPtr hObject);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetProcessWindowStation();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetThreadDesktop(
|
||||
int dwThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern int GetCurrentThreadId();
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool GetTokenInformation(
|
||||
IntPtr TokenHandle,
|
||||
TokenInformationClass TokenInformationClass,
|
||||
IntPtr TokenInformation,
|
||||
uint TokenInformationLength,
|
||||
out uint ReturnLength);
|
||||
|
||||
[DllImport("psapi.dll", SetLastError = true)]
|
||||
private static extern bool EnumProcesses(
|
||||
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U4)]
|
||||
[In][Out] IntPtr[] processIds,
|
||||
uint cb,
|
||||
[MarshalAs(UnmanagedType.U4)]
|
||||
out uint pBytesReturned);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(
|
||||
ProcessAccessFlags processAccess,
|
||||
bool bInheritHandle,
|
||||
IntPtr processId);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool OpenProcessToken(
|
||||
IntPtr ProcessHandle,
|
||||
TokenAccessLevels DesiredAccess,
|
||||
out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool ConvertSidToStringSidW(
|
||||
IntPtr pSID,
|
||||
[MarshalAs(UnmanagedType.LPTStr)]
|
||||
out string StringSid);
|
||||
|
||||
[DllImport("advapi32", SetLastError = true)]
|
||||
private static extern bool DuplicateTokenEx(
|
||||
IntPtr hExistingToken,
|
||||
TokenAccessLevels dwDesiredAccess,
|
||||
IntPtr lpTokenAttributes,
|
||||
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
|
||||
TOKEN_TYPE TokenType,
|
||||
out IntPtr phNewToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool ImpersonateLoggedOnUser(
|
||||
IntPtr hToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool RevertToSelf();
|
||||
|
||||
public static CommandResult RunAsUser(string username, string password, string lpCommandLine,
|
||||
string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType)
|
||||
{
|
||||
SecurityIdentifier account = null;
|
||||
if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
|
||||
{
|
||||
account = GetBecomeSid(username);
|
||||
}
|
||||
|
||||
STARTUPINFOEX si = new STARTUPINFOEX();
|
||||
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
|
||||
|
||||
SECURITY_ATTRIBUTES pipesec = new SECURITY_ATTRIBUTES();
|
||||
pipesec.bInheritHandle = true;
|
||||
|
||||
// Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
|
||||
SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write;
|
||||
if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
|
||||
throw new Win32Exception("STDOUT pipe setup failed");
|
||||
if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDOUT pipe handle setup failed");
|
||||
|
||||
if (!CreatePipe(out stderr_read, out stderr_write, pipesec, 0))
|
||||
throw new Win32Exception("STDERR pipe setup failed");
|
||||
if (!SetHandleInformation(stderr_read, HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDERR pipe handle setup failed");
|
||||
|
||||
if (!CreatePipe(out stdin_read, out stdin_write, pipesec, 0))
|
||||
throw new Win32Exception("STDIN pipe setup failed");
|
||||
if (!SetHandleInformation(stdin_write, HandleFlags.INHERIT, 0))
|
||||
throw new Win32Exception("STDIN pipe handle setup failed");
|
||||
|
||||
si.startupInfo.hStdOutput = stdout_write;
|
||||
si.startupInfo.hStdError = stderr_write;
|
||||
si.startupInfo.hStdInput = stdin_read;
|
||||
|
||||
// Setup the stdin buffer
|
||||
UTF8Encoding utf8_encoding = new UTF8Encoding(false);
|
||||
FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768);
|
||||
StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
|
||||
|
||||
// Create the environment block if set
|
||||
IntPtr lpEnvironment = IntPtr.Zero;
|
||||
|
||||
CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT;
|
||||
|
||||
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
||||
|
||||
// Get the user tokens to try running processes with
|
||||
List<IntPtr> tokens = GetUserTokens(account, username, password, logonType);
|
||||
|
||||
bool launch_success = false;
|
||||
foreach (IntPtr token in tokens)
|
||||
{
|
||||
if (CreateProcessWithTokenW(
|
||||
token,
|
||||
logonFlags,
|
||||
null,
|
||||
new StringBuilder(lpCommandLine),
|
||||
startup_flags,
|
||||
lpEnvironment,
|
||||
lpCurrentDirectory,
|
||||
si,
|
||||
out pi))
|
||||
{
|
||||
launch_success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!launch_success)
|
||||
throw new Win32Exception("Failed to start become process");
|
||||
|
||||
CommandResult result = new CommandResult();
|
||||
// Setup the output buffers and get stdout/stderr
|
||||
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096);
|
||||
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
|
||||
stdout_write.Close();
|
||||
|
||||
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096);
|
||||
StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
|
||||
stderr_write.Close();
|
||||
|
||||
stdin.WriteLine(stdinInput);
|
||||
stdin.Close();
|
||||
|
||||
string stdout_str, stderr_str = null;
|
||||
GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
|
||||
UInt32 rc = GetProcessExitCode(pi.hProcess);
|
||||
|
||||
result.StandardOut = stdout_str;
|
||||
result.StandardError = stderr_str;
|
||||
result.ExitCode = rc;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static SecurityIdentifier GetBecomeSid(string username)
|
||||
{
|
||||
NTAccount account = new NTAccount(username);
|
||||
try
|
||||
{
|
||||
SecurityIdentifier security_identifier = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier));
|
||||
return security_identifier;
|
||||
}
|
||||
catch (IdentityNotMappedException ex)
|
||||
{
|
||||
throw new Exception(String.Format("Unable to find become user {0}: {1}", username, ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<IntPtr> GetUserTokens(SecurityIdentifier account, string username, string password, LogonType logonType)
|
||||
{
|
||||
List<IntPtr> tokens = new List<IntPtr>();
|
||||
List<String> service_sids = new List<String>()
|
||||
{
|
||||
"S-1-5-18", // NT AUTHORITY\SYSTEM
|
||||
"S-1-5-19", // NT AUTHORITY\LocalService
|
||||
"S-1-5-20" // NT AUTHORITY\NetworkService
|
||||
};
|
||||
|
||||
IntPtr hSystemToken = IntPtr.Zero;
|
||||
string account_sid = "";
|
||||
if (logonType != LogonType.LOGON32_LOGON_NEW_CREDENTIALS)
|
||||
{
|
||||
GrantAccessToWindowStationAndDesktop(account);
|
||||
// Try to get SYSTEM token handle so we can impersonate to get full admin token
|
||||
hSystemToken = GetSystemUserHandle();
|
||||
account_sid = account.ToString();
|
||||
}
|
||||
bool impersonated = false;
|
||||
|
||||
try
|
||||
{
|
||||
IntPtr hSystemTokenDup = IntPtr.Zero;
|
||||
if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid))
|
||||
{
|
||||
// We need the SYSTEM token if we want to become one of those accounts, fail here
|
||||
throw new Win32Exception("Failed to get token for NT AUTHORITY\\SYSTEM");
|
||||
}
|
||||
else if (hSystemToken != IntPtr.Zero)
|
||||
{
|
||||
// We have the token, need to duplicate and impersonate
|
||||
bool dupResult = DuplicateTokenEx(
|
||||
hSystemToken,
|
||||
TokenAccessLevels.MaximumAllowed,
|
||||
IntPtr.Zero,
|
||||
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
|
||||
TOKEN_TYPE.TokenPrimary,
|
||||
out hSystemTokenDup);
|
||||
int lastError = Marshal.GetLastWin32Error();
|
||||
CloseHandle(hSystemToken);
|
||||
|
||||
if (!dupResult && service_sids.Contains(account_sid))
|
||||
throw new Win32Exception(lastError, "Failed to duplicate token for NT AUTHORITY\\SYSTEM");
|
||||
else if (dupResult && account_sid != "S-1-5-18")
|
||||
{
|
||||
if (ImpersonateLoggedOnUser(hSystemTokenDup))
|
||||
impersonated = true;
|
||||
else if (service_sids.Contains(account_sid))
|
||||
throw new Win32Exception("Failed to impersonate as SYSTEM account");
|
||||
}
|
||||
// If SYSTEM impersonation failed but we're trying to become a regular user, just proceed;
|
||||
// might get a limited token in UAC-enabled cases, but better than nothing...
|
||||
}
|
||||
|
||||
string domain = null;
|
||||
|
||||
if (service_sids.Contains(account_sid))
|
||||
{
|
||||
// We're using a well-known service account, do a service logon instead of the actual flag set
|
||||
logonType = LogonType.LOGON32_LOGON_SERVICE;
|
||||
domain = "NT AUTHORITY";
|
||||
password = null;
|
||||
switch (account_sid)
|
||||
{
|
||||
case "S-1-5-18":
|
||||
tokens.Add(hSystemTokenDup);
|
||||
return tokens;
|
||||
case "S-1-5-19":
|
||||
username = "LocalService";
|
||||
break;
|
||||
case "S-1-5-20":
|
||||
username = "NetworkService";
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We are trying to become a local or domain account
|
||||
if (username.Contains(@"\"))
|
||||
{
|
||||
var user_split = username.Split(Convert.ToChar(@"\"));
|
||||
domain = user_split[0];
|
||||
username = user_split[1];
|
||||
}
|
||||
else if (username.Contains("@"))
|
||||
domain = null;
|
||||
else
|
||||
domain = ".";
|
||||
}
|
||||
|
||||
IntPtr hToken = IntPtr.Zero;
|
||||
if (!LogonUser(
|
||||
username,
|
||||
domain,
|
||||
password,
|
||||
logonType,
|
||||
LogonProvider.LOGON32_PROVIDER_DEFAULT,
|
||||
out hToken))
|
||||
{
|
||||
throw new Win32Exception("LogonUser failed");
|
||||
}
|
||||
|
||||
if (!service_sids.Contains(account_sid))
|
||||
{
|
||||
// Try and get the elevated token for local/domain account
|
||||
IntPtr hTokenElevated = GetElevatedToken(hToken);
|
||||
tokens.Add(hTokenElevated);
|
||||
}
|
||||
|
||||
// add the original token as a fallback
|
||||
tokens.Add(hToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (impersonated)
|
||||
RevertToSelf();
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static IntPtr GetSystemUserHandle()
|
||||
{
|
||||
uint array_byte_size = 1024 * sizeof(uint);
|
||||
IntPtr[] pids = new IntPtr[1024];
|
||||
uint bytes_copied;
|
||||
|
||||
if (!EnumProcesses(pids, array_byte_size, out bytes_copied))
|
||||
{
|
||||
throw new Win32Exception("Failed to enumerate processes");
|
||||
}
|
||||
// TODO: Handle if bytes_copied is larger than the array size and rerun EnumProcesses with larger array
|
||||
uint num_processes = bytes_copied / sizeof(uint);
|
||||
|
||||
for (uint i = 0; i < num_processes; i++)
|
||||
{
|
||||
IntPtr hProcess = OpenProcess(ProcessAccessFlags.PROCESS_QUERY_INFORMATION, false, pids[i]);
|
||||
if (hProcess != IntPtr.Zero)
|
||||
{
|
||||
IntPtr hToken = IntPtr.Zero;
|
||||
// According to CreateProcessWithTokenW we require a token with
|
||||
// TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY
|
||||
// Also add in TOKEN_IMPERSONATE so we can get an impersontated token
|
||||
TokenAccessLevels desired_access = TokenAccessLevels.Query |
|
||||
TokenAccessLevels.Duplicate |
|
||||
TokenAccessLevels.AssignPrimary |
|
||||
TokenAccessLevels.Impersonate;
|
||||
|
||||
if (OpenProcessToken(hProcess, desired_access, out hToken))
|
||||
{
|
||||
string sid = GetTokenUserSID(hToken);
|
||||
if (sid == "S-1-5-18")
|
||||
{
|
||||
CloseHandle(hProcess);
|
||||
return hToken;
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
CloseHandle(hProcess);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static string GetTokenUserSID(IntPtr hToken)
|
||||
{
|
||||
uint token_length;
|
||||
string sid;
|
||||
|
||||
if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, IntPtr.Zero, 0, out token_length))
|
||||
{
|
||||
int last_err = Marshal.GetLastWin32Error();
|
||||
if (last_err != 122) // ERROR_INSUFFICIENT_BUFFER
|
||||
throw new Win32Exception(last_err, "Failed to get TokenUser length");
|
||||
}
|
||||
|
||||
IntPtr token_information = Marshal.AllocHGlobal((int)token_length);
|
||||
try
|
||||
{
|
||||
if (!GetTokenInformation(hToken, TokenInformationClass.TokenUser, token_information, token_length, out token_length))
|
||||
throw new Win32Exception("Failed to get TokenUser information");
|
||||
|
||||
TOKEN_USER token_user = (TOKEN_USER)Marshal.PtrToStructure(token_information, typeof(TOKEN_USER));
|
||||
|
||||
if (!ConvertSidToStringSidW(token_user.User.Sid, out sid))
|
||||
throw new Win32Exception("Failed to get user SID");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(token_information);
|
||||
}
|
||||
|
||||
return sid;
|
||||
}
|
||||
|
||||
private static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr)
|
||||
{
|
||||
var sowait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
var sewait = new EventWaitHandle(false, EventResetMode.ManualReset);
|
||||
string so = null, se = null;
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
so = stdoutStream.ReadToEnd();
|
||||
sowait.Set();
|
||||
});
|
||||
ThreadPool.QueueUserWorkItem((s) =>
|
||||
{
|
||||
se = stderrStream.ReadToEnd();
|
||||
sewait.Set();
|
||||
});
|
||||
foreach (var wh in new WaitHandle[] { sowait, sewait })
|
||||
wh.WaitOne();
|
||||
stdout = so;
|
||||
stderr = se;
|
||||
}
|
||||
|
||||
private static uint GetProcessExitCode(IntPtr processHandle)
|
||||
{
|
||||
new NativeWaitHandle(processHandle).WaitOne();
|
||||
uint exitCode;
|
||||
if (!GetExitCodeProcess(processHandle, out exitCode))
|
||||
throw new Win32Exception("Error getting process exit code");
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
private static IntPtr GetElevatedToken(IntPtr hToken)
|
||||
{
|
||||
uint requestedLength;
|
||||
|
||||
IntPtr pTokenInfo = Marshal.AllocHGlobal(sizeof(int));
|
||||
|
||||
try
|
||||
{
|
||||
if (!GetTokenInformation(hToken, TokenInformationClass.TokenElevationType, pTokenInfo, sizeof(int), out requestedLength))
|
||||
throw new Win32Exception("Unable to get TokenElevationType");
|
||||
|
||||
var tet = (TokenElevationType)Marshal.ReadInt32(pTokenInfo);
|
||||
|
||||
// we already have the best token we can get, just use it
|
||||
if (tet != TokenElevationType.TokenElevationTypeLimited)
|
||||
return hToken;
|
||||
|
||||
GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, IntPtr.Zero, 0, out requestedLength);
|
||||
|
||||
IntPtr pLinkedToken = Marshal.AllocHGlobal((int)requestedLength);
|
||||
|
||||
if (!GetTokenInformation(hToken, TokenInformationClass.TokenLinkedToken, pLinkedToken, requestedLength, out requestedLength))
|
||||
throw new Win32Exception("Unable to get linked token");
|
||||
|
||||
IntPtr linkedToken = Marshal.ReadIntPtr(pLinkedToken);
|
||||
|
||||
Marshal.FreeHGlobal(pLinkedToken);
|
||||
|
||||
return linkedToken;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(pTokenInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GrantAccessToWindowStationAndDesktop(SecurityIdentifier account)
|
||||
{
|
||||
const int WindowStationAllAccess = 0x000f037f;
|
||||
GrantAccess(account, GetProcessWindowStation(), WindowStationAllAccess);
|
||||
const int DesktopRightsAllAccess = 0x000f01ff;
|
||||
GrantAccess(account, GetThreadDesktop(GetCurrentThreadId()), DesktopRightsAllAccess);
|
||||
}
|
||||
|
||||
private static void GrantAccess(SecurityIdentifier account, IntPtr handle, int accessMask)
|
||||
{
|
||||
SafeHandle safeHandle = new NoopSafeHandle(handle);
|
||||
GenericSecurity security =
|
||||
new GenericSecurity(false, ResourceType.WindowObject, safeHandle, AccessControlSections.Access);
|
||||
security.AddAccessRule(
|
||||
new GenericAccessRule(account, accessMask, AccessControlType.Allow));
|
||||
security.Persist(safeHandle, AccessControlSections.Access);
|
||||
}
|
||||
|
||||
private class GenericSecurity : NativeObjectSecurity
|
||||
{
|
||||
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested)
|
||||
: base(isContainer, resType, objectHandle, sectionsRequested) { }
|
||||
public new void Persist(SafeHandle handle, AccessControlSections includeSections) { base.Persist(handle, includeSections); }
|
||||
public new void AddAccessRule(AccessRule rule) { base.AddAccessRule(rule); }
|
||||
public override Type AccessRightType { get { throw new NotImplementedException(); } }
|
||||
public override AccessRule AccessRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited,
|
||||
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AccessControlType type)
|
||||
{ throw new NotImplementedException(); }
|
||||
public override Type AccessRuleType { get { return typeof(AccessRule); } }
|
||||
public override AuditRule AuditRuleFactory(System.Security.Principal.IdentityReference identityReference, int accessMask, bool isInherited,
|
||||
InheritanceFlags inheritanceFlags, PropagationFlags propagationFlags, AuditFlags flags)
|
||||
{ throw new NotImplementedException(); }
|
||||
public override Type AuditRuleType { get { return typeof(AuditRule); } }
|
||||
}
|
||||
|
||||
private class NoopSafeHandle : SafeHandle
|
||||
{
|
||||
public NoopSafeHandle(IntPtr handle) : base(handle, false) { }
|
||||
public override bool IsInvalid { get { return false; } }
|
||||
protected override bool ReleaseHandle() { return true; }
|
||||
}
|
||||
|
||||
private class GenericAccessRule : AccessRule
|
||||
{
|
||||
public GenericAccessRule(IdentityReference identity, int accessMask, AccessControlType type) :
|
||||
base(identity, accessMask, false, InheritanceFlags.None, PropagationFlags.None, type)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
# 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
|
||||
TODO - This is an AnsibleModule object that is used to derive the
|
||||
TempPath and Debug values.
|
||||
TempPath is set to the TmpDir property of the class
|
||||
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.
|
||||
#>
|
||||
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
|
||||
)
|
||||
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]@()
|
||||
$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
|
||||
}
|
||||
|
||||
# pattern used to find referenced assemblies in the code
|
||||
$assembly_pattern = "^//\s*AssemblyReference\s+-Name\s+(?<Name>[\w.]*)(\s+-CLR\s+(?<CLR>Core|Framework))?$"
|
||||
|
||||
# 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")
|
||||
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
|
||||
$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]
|
||||
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
|
||||
try {
|
||||
while ($null -ne ($line = $sr.ReadLine())) {
|
||||
if ($line -imatch $assembly_pattern) {
|
||||
# verify the reference is not for .NET Framework
|
||||
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Core") {
|
||||
continue
|
||||
}
|
||||
$assemblies.Add($Matches.Name) > $null
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$sr.Close()
|
||||
}
|
||||
$syntax_trees.Add([Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree]::ParseText($reference, $parse_options)) > $null
|
||||
}
|
||||
|
||||
# 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)
|
||||
}
|
||||
|
||||
# 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.CompilerOptions = [String]::Join(" ", $compiler_options.ToArray())
|
||||
$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
|
||||
# //AssemblyReference -Name ... [-CLR Framework]
|
||||
$compile_units = [System.Collections.Generic.List`1[System.CodeDom.CodeSnippetCompileUnit]]@()
|
||||
foreach ($reference in $References) {
|
||||
$sr = New-Object -TypeName System.IO.StringReader -ArgumentList $reference
|
||||
try {
|
||||
while ($null -ne ($line = $sr.ReadLine())) {
|
||||
if ($line -imatch $assembly_pattern) {
|
||||
# verify the reference is not for .NET Core
|
||||
if ($Matches.ContainsKey("CLR") -and $Matches.CLR -ne "Framework") {
|
||||
continue
|
||||
}
|
||||
$assemblies.Add($Matches.Name) > $null
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$sr.Close()
|
||||
}
|
||||
$compile_units.Add((New-Object -TypeName System.CodeDom.CodeSnippetCompileUnit -ArgumentList $reference)) > $null
|
||||
}
|
||||
$compile_parameters.ReferencedAssemblies.AddRange($assemblies)
|
||||
|
||||
# compile the code together and check for errors
|
||||
$provider = New-Object -TypeName Microsoft.CSharp.CSharpCodeProvider
|
||||
$compile = $provider.CompileAssemblyFromDom($compile_parameters, $compile_units.ToArray())
|
||||
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
|
||||
}
|
||||
|
||||
# return the compiled assembly if PassThru is set.
|
||||
if ($PassThru) {
|
||||
return $compiled_assembly
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Add-CSharpType
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
|
||||
#!powershell
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Function Assert-Equals($actual, $expected) {
|
||||
if ($actual -cne $expected) {
|
||||
$call_stack = (Get-PSCallStack)[1]
|
||||
$error_msg = "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: $($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
|
||||
Fail-Json -obj $result -message $error_msg
|
||||
}
|
||||
}
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
#ConvertFrom-AnsibleJso
|
||||
$input_json = '{"string":"string","float":3.1415926,"dict":{"string":"string","int":1},"list":["entry 1","entry 2"],"null":null,"int":1}'
|
||||
$actual = ConvertFrom-AnsibleJson -InputObject $input_json
|
||||
Assert-Equals -actual $actual.GetType() -expected ([Hashtable])
|
||||
Assert-Equals -actual $actual.string.GetType() -expected ([String])
|
||||
Assert-Equals -actual $actual.string -expected "string"
|
||||
Assert-Equals -actual $actual.int.GetType() -expected ([Int32])
|
||||
Assert-Equals -actual $actual.int -expected 1
|
||||
Assert-Equals -actual $actual.null -expected $null
|
||||
Assert-Equals -actual $actual.float.GetType() -expected ([Decimal])
|
||||
Assert-Equals -actual $actual.float -expected 3.1415926
|
||||
Assert-Equals -actual $actual.list.GetType() -expected ([Object[]])
|
||||
Assert-Equals -actual $actual.list.Count -expected 2
|
||||
Assert-Equals -actual $actual.list[0] -expected "entry 1"
|
||||
Assert-Equals -actual $actual.list[1] -expected "entry 2"
|
||||
Assert-Equals -actual $actual.GetType() -expected ([Hashtable])
|
||||
Assert-Equals -actual $actual.dict.string -expected "string"
|
||||
Assert-Equals -actual $actual.dict.int -expected 1
|
||||
|
||||
$result.msg = "good"
|
||||
Exit-Json -obj $result
|
||||
|
@ -0,0 +1,58 @@
|
||||
#!powershell
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
|
||||
$data = Get-AnsibleParam -obj $params -name "data" -type "str" -default "normal"
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
<#
|
||||
This module tests various error events in PowerShell to verify our hidden trap
|
||||
catches them all and outputs a pretty error message with a traceback to help
|
||||
users debug the actual issue
|
||||
|
||||
normal - normal execution, no errors
|
||||
fail - Calls Fail-Json like normal
|
||||
throw - throws an exception
|
||||
error - Write-Error with ErrorActionPreferenceStop
|
||||
cmdlet_error - Calls a Cmdlet with an invalid error
|
||||
dotnet_exception - Calls a .NET function that will throw an error
|
||||
function_throw - Throws an exception in a function
|
||||
proc_exit_fine - calls an executable with a non-zero exit code with Exit-Json
|
||||
proc_exit_fail - calls an executable with a non-zero exit code with Fail-Json
|
||||
#>
|
||||
|
||||
Function Test-ThrowException {
|
||||
throw "exception in function"
|
||||
}
|
||||
|
||||
if ($data -eq "normal") {
|
||||
Exit-Json -obj $result
|
||||
} elseif ($data -eq "fail") {
|
||||
Fail-Json -obj $result -message "fail message"
|
||||
} elseif ($data -eq "throw") {
|
||||
throw [ArgumentException]"module is thrown"
|
||||
} elseif ($data -eq "error") {
|
||||
Write-Error -Message $data
|
||||
} elseif ($data -eq "cmdlet_error") {
|
||||
Get-Item -Path "fake:\path"
|
||||
} elseif ($data -eq "dotnet_exception") {
|
||||
[System.IO.Path]::GetFullPath($null)
|
||||
} elseif ($data -eq "function_throw") {
|
||||
Test-ThrowException
|
||||
} elseif ($data -eq "proc_exit_fine") {
|
||||
# verifies that if no error was actually fired and we have an output, we
|
||||
# don't use the RC to validate if the module failed
|
||||
&cmd.exe /c exit 2
|
||||
Exit-Json -obj $result
|
||||
} elseif ($data -eq "proc_exit_fail") {
|
||||
&cmd.exe /c exit 2
|
||||
Fail-Json -obj $result -message "proc_exit_fail"
|
||||
}
|
||||
|
||||
# verify no exception were silently caught during our tests
|
||||
Fail-Json -obj $result -message "end of module"
|
||||
|
@ -0,0 +1,187 @@
|
||||
#!powershell
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.AddType
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
Function Assert-Equals($actual, $expected) {
|
||||
if ($actual -cne $expected) {
|
||||
$call_stack = (Get-PSCallStack)[1]
|
||||
$error_msg = "AssertionError:`r`nActual: `"$actual`" != Expected: `"$expected`"`r`nLine: $($call_stack.ScriptLineNumber), Method: $($call_stack.Position.Text)"
|
||||
Fail-Json -obj $result -message $error_msg
|
||||
}
|
||||
}
|
||||
|
||||
$code = @'
|
||||
using System;
|
||||
|
||||
namespace Namespace1
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
public static string GetString(bool error)
|
||||
{
|
||||
if (error)
|
||||
throw new Exception("error");
|
||||
return "Hello World";
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
$res = Add-CSharpType -References $code
|
||||
Assert-Equals -actual $res -expected $null
|
||||
|
||||
$actual = [Namespace1.Class1]::GetString($false)
|
||||
Assert-Equals $actual -expected "Hello World"
|
||||
|
||||
try {
|
||||
[Namespace1.Class1]::GetString($true)
|
||||
} catch {
|
||||
Assert-Equals ($_.Exception.ToString().Contains("at Namespace1.Class1.GetString(Boolean error)`r`n")) -expected $true
|
||||
}
|
||||
|
||||
$code_debug = @'
|
||||
using System;
|
||||
|
||||
namespace Namespace2
|
||||
{
|
||||
public class Class2
|
||||
{
|
||||
public static string GetString(bool error)
|
||||
{
|
||||
if (error)
|
||||
throw new Exception("error");
|
||||
return "Hello World";
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
$res = Add-CSharpType -References $code_debug -IncludeDebugInfo
|
||||
Assert-Equals -actual $res -expected $null
|
||||
|
||||
$actual = [Namespace2.Class2]::GetString($false)
|
||||
Assert-Equals $actual -expected "Hello World"
|
||||
|
||||
try {
|
||||
[Namespace2.Class2]::GetString($true)
|
||||
} catch {
|
||||
$tmp_path = [System.IO.Path]::GetFullPath($env:TMP).ToLower()
|
||||
Assert-Equals ($_.Exception.ToString().ToLower().Contains("at namespace2.class2.getstring(boolean error) in $tmp_path")) -expected $true
|
||||
Assert-Equals ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
|
||||
}
|
||||
|
||||
$code_tmp = @'
|
||||
using System;
|
||||
|
||||
namespace Namespace3
|
||||
{
|
||||
public class Class3
|
||||
{
|
||||
public static string GetString(bool error)
|
||||
{
|
||||
if (error)
|
||||
throw new Exception("error");
|
||||
return "Hello World";
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
$tmp_path = $env:USERPROFILE
|
||||
$res = Add-CSharpType -References $code_tmp -IncludeDebugInfo -TempPath $tmp_path -PassThru
|
||||
Assert-Equals -actual $res.GetType().Name -expected "RuntimeAssembly"
|
||||
Assert-Equals -actual $res.Location -expected ""
|
||||
Assert-Equals -actual $res.GetTypes().Length -expected 1
|
||||
Assert-Equals -actual $res.GetTypes()[0].Name -expected "Class3"
|
||||
|
||||
$actual = [Namespace3.Class3]::GetString($false)
|
||||
Assert-Equals $actual -expected "Hello World"
|
||||
|
||||
try {
|
||||
[Namespace3.Class3]::GetString($true)
|
||||
} catch {
|
||||
Assert-Equals ($_.Exception.ToString().ToLower().Contains("at namespace3.class3.getstring(boolean error) in $($tmp_path.ToLower())")) -expected $true
|
||||
Assert-Equals ($_.Exception.ToString().Contains(".cs:line 10")) -expected $true
|
||||
}
|
||||
|
||||
$warning_code = @'
|
||||
using System;
|
||||
|
||||
namespace Namespace4
|
||||
{
|
||||
public class Class4
|
||||
{
|
||||
public static string GetString(bool test)
|
||||
{
|
||||
if (test)
|
||||
{
|
||||
string a = "";
|
||||
}
|
||||
|
||||
return "Hello World";
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
$failed = $false
|
||||
try {
|
||||
Add-CSharpType -References $warning_code
|
||||
} catch {
|
||||
$failed = $true
|
||||
Assert-Equals -actual ($_.Exception.Message.Contains("error CS0219: Warning as Error: The variable 'a' is assigned but its value is never used")) -expected $true
|
||||
}
|
||||
Assert-Equals -actual $failed -expected $true
|
||||
|
||||
Add-CSharpType -References $warning_code -IgnoreWarnings
|
||||
$actual = [Namespace4.Class4]::GetString($true)
|
||||
Assert-Equals -actual $actual -expected "Hello World"
|
||||
|
||||
$reference_1 = @'
|
||||
using System;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
//AssemblyReference -Name System.Web.Extensions.dll
|
||||
|
||||
namespace Namespace5
|
||||
{
|
||||
public class Class5
|
||||
{
|
||||
public static string GetString()
|
||||
{
|
||||
return "Hello World";
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
$reference_2 = @'
|
||||
using System;
|
||||
using Namespace5;
|
||||
using System.Management.Automation;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Namespace6
|
||||
{
|
||||
public class Class6
|
||||
{
|
||||
public static string GetString()
|
||||
{
|
||||
Hashtable hash = new Hashtable();
|
||||
hash["test"] = "abc";
|
||||
return Class5.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
Add-CSharpType -References $reference_1, $reference_2
|
||||
$actual = [Namespace6.Class6]::GetString()
|
||||
Assert-Equals -actual $actual -expected "Hello World"
|
||||
|
||||
$result.res = "success"
|
||||
Exit-Json -obj $result
|
@ -0,0 +1,12 @@
|
||||
#1powershell
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#AnsibleRequires -CSharpUtil Ansible.Test
|
||||
|
||||
$result = @{
|
||||
res = [Ansible.Test.OutputTest]::GetString()
|
||||
changed = $false
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
|
@ -0,0 +1,26 @@
|
||||
//AssemblyReference -Name System.Web.Extensions.dll
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace Ansible.Test
|
||||
{
|
||||
public class OutputTest
|
||||
{
|
||||
public static string GetString()
|
||||
{
|
||||
Dictionary<string, object> obj = new Dictionary<string, object>();
|
||||
obj["a"] = "a";
|
||||
obj["b"] = 1;
|
||||
return ToJson(obj);
|
||||
}
|
||||
|
||||
private static string ToJson(object obj)
|
||||
{
|
||||
JavaScriptSerializer jss = new JavaScriptSerializer();
|
||||
return jss.Serialize(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
"""Analyze C# import statements."""
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from lib.util import (
|
||||
display,
|
||||
)
|
||||
|
||||
|
||||
def get_csharp_module_utils_imports(powershell_targets, csharp_targets):
|
||||
"""Return a dictionary of module_utils names mapped to sets of powershell file paths.
|
||||
:type powershell_targets: list[TestTarget] - C# files
|
||||
:type csharp_targets: list[TestTarget] - PS files
|
||||
:rtype: dict[str, set[str]]
|
||||
"""
|
||||
|
||||
module_utils = enumerate_module_utils()
|
||||
|
||||
imports_by_target_path = {}
|
||||
|
||||
for target in powershell_targets:
|
||||
imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, False)
|
||||
|
||||
for target in csharp_targets:
|
||||
imports_by_target_path[target.path] = extract_csharp_module_utils_imports(target.path, module_utils, True)
|
||||
|
||||
imports = dict([(module_util, set()) for module_util in module_utils])
|
||||
|
||||
for target_path in imports_by_target_path:
|
||||
for module_util in imports_by_target_path[target_path]:
|
||||
imports[module_util].add(target_path)
|
||||
|
||||
for module_util in sorted(imports):
|
||||
if not imports[module_util]:
|
||||
display.warning('No imports found which use the "%s" module_util.' % module_util)
|
||||
|
||||
return imports
|
||||
|
||||
|
||||
def enumerate_module_utils():
|
||||
"""Return a list of available module_utils imports.
|
||||
:rtype: set[str]
|
||||
"""
|
||||
return set(os.path.splitext(p)[0] for p in os.listdir('lib/ansible/module_utils/csharp') if os.path.splitext(p)[1] == '.cs')
|
||||
|
||||
|
||||
def extract_csharp_module_utils_imports(path, module_utils, is_pure_csharp):
|
||||
"""Return a list of module_utils imports found in the specified source file.
|
||||
:type path: str
|
||||
:type module_utils: set[str]
|
||||
:rtype: set[str]
|
||||
"""
|
||||
imports = set()
|
||||
if is_pure_csharp:
|
||||
pattern = re.compile(r'(?i)^using\s(Ansible\..+);$')
|
||||
else:
|
||||
pattern = re.compile(r'(?i)^#\s*ansiblerequires\s+-csharputil\s+(Ansible\..+)')
|
||||
|
||||
with open(path, 'r') as module_file:
|
||||
for line_number, line in enumerate(module_file, 1):
|
||||
match = re.search(pattern, line)
|
||||
|
||||
if not match:
|
||||
continue
|
||||
|
||||
import_name = match.group(1)
|
||||
if import_name not in module_utils:
|
||||
display.warning('%s:%d Invalid module_utils import: %s' % (path, line_number, import_name))
|
||||
continue
|
||||
|
||||
imports.add(import_name)
|
||||
|
||||
return imports
|
Loading…
Reference in New Issue