1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

runas + async - get working on older hosts (#41772)

* runas + async - get working on older hosts

* fixed up sanity issues

* Moved first task to end of test for CI race issues

* Minor change to async test to be more stable, change to runas become to not touch the disk

* moved async test back to normal spot
This commit is contained in:
Jordan Borean 2018-06-26 14:40:49 +10:00 committed by GitHub
parent 57ea4cafff
commit 2af36412f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 186 additions and 640 deletions

View file

@ -0,0 +1,3 @@
bugfixes:
- winrm - running async with become on a Server 2008 or 2008 R2 host will now work
- become runas - changed runas process so it does not create a temporary file on the disk during execution

View file

@ -563,7 +563,7 @@ Limitations
Be aware of the following limitations with ``become`` on Windows: Be aware of the following limitations with ``become`` on Windows:
* Running a task with ``async`` and ``become`` on Windows Server 2008, 2008 R2 * Running a task with ``async`` and ``become`` on Windows Server 2008, 2008 R2
and Windows 7 does not work. and Windows 7 only works when using Ansible 2.7 or newer.
* By default, the become user logs on with an interactive session, so it must * By default, the become user logs on with an interactive session, so it must
have the right to do so on the Windows host. If it does not inherit the have the right to do so on the Windows host. If it does not inherit the

View file

@ -279,30 +279,6 @@ namespace Ansible
public SID_AND_ATTRIBUTES User; public SID_AND_ATTRIBUTES User;
} }
[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public UInt64 PerProcessUserTimeLimit;
public UInt64 PerJobUserTimeLimit;
public LimitFlags LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public UInt32 ActiveProcessLimit;
public UIntPtr Affinity;
public UInt32 PriorityClass;
public UInt32 SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
public class JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
[MarshalAs(UnmanagedType.ByValArray, SizeConst=48)]
public byte[] IO_COUNTERS_BLOB;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=4)]
public UIntPtr[] LIMIT_BLOB;
}
[Flags] [Flags]
public enum StartupInfoFlags : uint public enum StartupInfoFlags : uint
{ {
@ -382,24 +358,6 @@ namespace Ansible
TokenImpersonation TokenImpersonation
} }
enum JobObjectInfoType
{
ExtendedLimitInformation = 9,
}
[Flags]
enum ThreadAccessRights : uint
{
SUSPEND_RESUME = 0x0002
}
[Flags]
public enum LimitFlags : uint
{
JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
}
class NativeWaitHandle : WaitHandle class NativeWaitHandle : WaitHandle
{ {
public NativeWaitHandle(IntPtr handle) public NativeWaitHandle(IntPtr handle)
@ -427,63 +385,6 @@ namespace Ansible
public uint ExitCode { get; internal set; } public uint ExitCode { get; internal set; }
} }
public class Job : IDisposable
{
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr CreateJobObject(
IntPtr lpJobAttributes,
string lpName);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetInformationJobObject(
IntPtr hJob,
JobObjectInfoType JobObjectInfoClass,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo,
int cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool AssignProcessToJobObject(
IntPtr hJob,
IntPtr hProcess);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(
IntPtr hObject);
private IntPtr handle;
public Job()
{
handle = CreateJobObject(IntPtr.Zero, null);
if (handle == IntPtr.Zero)
throw new Win32Exception("CreateJobObject() failed");
JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedJobInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
// on OSs that support nested jobs, one of the jobs must allow breakaway for async to work properly under WinRM
extendedJobInfo.BasicLimitInformation.LimitFlags = LimitFlags.JOB_OBJECT_LIMIT_BREAKAWAY_OK | LimitFlags.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedJobInfo, Marshal.SizeOf(extendedJobInfo)))
throw new Win32Exception("SetInformationJobObject() failed");
}
public void AssignProcess(IntPtr processHandle)
{
if (!AssignProcessToJobObject(handle, processHandle))
throw new Win32Exception("AssignProcessToJobObject() failed");
}
public void Dispose()
{
if (handle != IntPtr.Zero)
{
CloseHandle(handle);
handle = IntPtr.Zero;
}
GC.SuppressFinalize(this);
}
}
public class BecomeUtil public class BecomeUtil
{ {
[DllImport("advapi32.dll", SetLastError = true)] [DllImport("advapi32.dll", SetLastError = true)]
@ -591,16 +492,6 @@ namespace Ansible
[DllImport("advapi32.dll", SetLastError = true)] [DllImport("advapi32.dll", SetLastError = true)]
private static extern bool RevertToSelf(); private static extern bool RevertToSelf();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeFileHandle OpenThread(
ThreadAccessRights dwDesiredAccess,
bool bInheritHandle,
int dwThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int ResumeThread(
SafeHandle hThread);
public static CommandResult RunAsUser(string username, string password, string lpCommandLine, public static CommandResult RunAsUser(string username, string password, string lpCommandLine,
string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType) string lpCurrentDirectory, string stdinInput, LogonFlags logonFlags, LogonType logonType)
{ {
@ -645,8 +536,7 @@ namespace Ansible
// Create the environment block if set // Create the environment block if set
IntPtr lpEnvironment = IntPtr.Zero; IntPtr lpEnvironment = IntPtr.Zero;
// To support async + become, we have to do some job magic later, which requires both breakaway and starting suspended CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT;
CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT | CreationFlags.CREATE_BREAKAWAY_FROM_JOB | CreationFlags.CREATE_SUSPENDED;
PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
@ -675,44 +565,26 @@ namespace Ansible
if (!launch_success) if (!launch_success)
throw new Win32Exception("Failed to start become process"); throw new Win32Exception("Failed to start become process");
// If 2012/8+ OS, create new job with JOB_OBJECT_LIMIT_BREAKAWAY_OK
// so that async can work
Job job = null;
if (Environment.OSVersion.Version >= new Version("6.2"))
{
job = new Job();
job.AssignProcess(pi.hProcess);
}
ResumeProcessById(pi.dwProcessId);
CommandResult result = new CommandResult(); CommandResult result = new CommandResult();
try // Setup the output buffers and get stdout/stderr
{ FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096);
// Setup the output buffers and get stdout/stderr StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096); stdout_write.Close();
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
stdout_write.Close();
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096); FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096);
StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096); StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
stderr_write.Close(); stderr_write.Close();
stdin.WriteLine(stdinInput); stdin.WriteLine(stdinInput);
stdin.Close(); stdin.Close();
string stdout_str, stderr_str = null; string stdout_str, stderr_str = null;
GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str); GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
UInt32 rc = GetProcessExitCode(pi.hProcess); UInt32 rc = GetProcessExitCode(pi.hProcess);
result.StandardOut = stdout_str; result.StandardOut = stdout_str;
result.StandardError = stderr_str; result.StandardError = stderr_str;
result.ExitCode = rc; result.ExitCode = rc;
}
finally
{
if (job != null)
job.Dispose();
}
return result; return result;
} }
@ -1013,44 +885,6 @@ namespace Ansible
security.Persist(safeHandle, AccessControlSections.Access); security.Persist(safeHandle, AccessControlSections.Access);
} }
private static void ResumeThreadById(int threadId)
{
var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId);
if (threadHandle.IsInvalid)
throw new Win32Exception(String.Format("Thread ID {0} is invalid", threadId));
try
{
if (ResumeThread(threadHandle) == -1)
throw new Win32Exception(String.Format("Thread ID {0} cannot be resumed", threadId));
}
finally
{
threadHandle.Dispose();
}
}
private static void ResumeProcessById(int pid)
{
var proc = Process.GetProcessById(pid);
// wait for at least one suspended thread in the process (this handles possible slow startup race where
// primary thread of created-suspended process has not yet become runnable)
var retryCount = 0;
while (!proc.Threads.OfType<ProcessThread>().Any(t => t.ThreadState == System.Diagnostics.ThreadState.Wait &&
t.WaitReason == ThreadWaitReason.Suspended))
{
proc.Refresh();
Thread.Sleep(50);
if (retryCount > 100)
throw new InvalidOperationException(String.Format("No threads were suspended in target PID {0} after 5s", pid));
}
foreach (var thread in proc.Threads.OfType<ProcessThread>().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait &&
t.WaitReason == ThreadWaitReason.Suspended))
ResumeThreadById(thread.Id);
}
private class GenericSecurity : NativeObjectSecurity private class GenericSecurity : NativeObjectSecurity
{ {
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested) public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested)
@ -1085,6 +919,17 @@ namespace Ansible
} }
"@ "@
# due to the command line size limitations of CreateProcessWithTokenW, we
# execute a simple PS script that executes our full exec_wrapper so no files
# touch the disk
$become_exec_wrapper = {
chcp.com 65001 > $null
$ProgressPreference = "SilentlyContinue"
$exec_wrapper_str = [System.Console]::In.ReadToEnd()
$exec_wrapper = [ScriptBlock]::Create($exec_wrapper_str)
&$exec_wrapper
}
$exec_wrapper = { $exec_wrapper = {
Set-StrictMode -Version 2 Set-StrictMode -Version 2
$DebugPreference = "Continue" $DebugPreference = "Continue"
@ -1102,61 +947,10 @@ $exec_wrapper = {
return $output return $output
} }
Function Invoke-Win32Api {
# Inspired by - Call a Win32 API in PowerShell without compiling C# code on
# the disk
# http://www.leeholmes.com/blog/2007/10/02/managing-ini-files-with-powershell/
# https://blogs.technet.microsoft.com/heyscriptingguy/2013/06/27/use-powershell-to-interact-with-the-windows-api-part-3/
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)] [String]$DllName,
[Parameter(Mandatory=$true)] [String]$MethodName,
[Parameter(Mandatory=$true)] [Type]$ReturnType,
[Parameter()] [Type[]]$ParameterTypes = [Type[]]@(),
[Parameter()] [Object[]]$Parameters = [Object[]]@()
)
$assembly = New-Object -TypeName System.Reflection.AssemblyName -ArgumentList "Win32ApiAssembly"
$dynamic_assembly = [AppDomain]::CurrentDomain.DefineDynamicAssembly($assembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)
$dynamic_module = $dynamic_assembly.DefineDynamicModule("Win32Module", $false)
$dynamic_type = $dynamic_module.DefineType("Win32Type", "Public, Class")
$dynamic_method = $dynamic_type.DefineMethod(
$MethodName,
[Reflection.MethodAttributes]"Public, Static",
$ReturnType,
$ParameterTypes
)
$constructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([String])
$custom_attributes = New-Object -TypeName Reflection.Emit.CustomAttributeBuilder -ArgumentList @(
$constructor,
$DllName
)
$dynamic_method.SetCustomAttribute($custom_attributes)
$win32_type = $dynamic_type.CreateType()
$win32_type::$MethodName.Invoke($Parameters)
}
# become process is run under a different console to the WinRM one so we
# need to set the UTF-8 codepage again, this also needs to be set before
# reading the stdin pipe that contains the module args specifying the
# remote_tmp to use. Instead this will use reflection when calling the Win32
# API no tmp files touch the disk
$invoke_args = @{
DllName = "kernel32.dll"
ReturnType = [bool]
ParameterTypes = @([UInt32])
Parameters = @(65001)
}
Invoke-Win32Api -MethodName SetConsoleCP @invoke_args > $null
Invoke-Win32Api -MethodName SetConsoleOutputCP @invoke_args > $null
# stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
# exec runspace, capture output, cleanup, return module output # exec runspace, capture output, cleanup, return module output. Do not change this as it is set become before being passed to the
# become process.
$json_raw = [System.Console]::In.ReadToEnd() $json_raw = ""
If (-not $json_raw) { If (-not $json_raw) {
Write-Error "no input given" -Category InvalidArgument Write-Error "no input given" -Category InvalidArgument
@ -1295,35 +1089,19 @@ Function Run($payload) {
return $null return $null
} }
# NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via filesystem # NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via small
$temp = [System.IO.Path]::Combine($remote_tmp, [System.IO.Path]::GetRandomFileName() + ".ps1") # wrapper which calls our read wrapper passed through stdin. Cannot use 'powershell -' as
$exec_wrapper.ToString() | Set-Content -Path $temp # the $ErrorActionPreference is always set to Stop and cannot be changed
$payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
$exec_wrapper = $exec_wrapper.ToString().Replace('$json_raw = ""', "`$json_raw = '$payload_string'")
$rc = 0 $rc = 0
$exec_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($become_exec_wrapper.ToString()))
$lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command")
$lp_current_directory = "$env:SystemRoot"
Try { Try {
# do not modify the ACL if the logon_type is LOGON32_LOGON_NEW_CREDENTIALS $result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type)
# as this results in the local execution running under the same user's token,
# otherwise we need to allow (potentially unprivileges) the become user access
# to the tempfile (NB: this likely won't work if traverse checking is enaabled).
if ($logon_type -ne [Ansible.LogonType]::LOGON32_LOGON_NEW_CREDENTIALS) {
$acl = Get-Acl -Path $temp
Try {
$acl.AddAccessRule($(New-Object System.Security.AccessControl.FileSystemAccessRule($username, "FullControl", "Allow")))
} Catch [System.Security.Principal.IdentityNotMappedException] {
throw "become_user '$username' is not recognized on this host"
} Catch {
throw "failed to set ACL on temp become execution script: $($_.Exception.Message)"
}
Set-Acl -Path $temp -AclObject $acl | Out-Null
}
$payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
$lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -File $temp")
$lp_current_directory = "$env:SystemRoot"
$result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $payload_string, $logon_flags, $logon_type)
$stdout = $result.StandardOut $stdout = $result.StandardOut
$stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim())) $stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim()))
$stderr = $result.StandardError $stderr = $result.StandardError
@ -1334,8 +1112,6 @@ Function Run($payload) {
} Catch { } Catch {
$excep = $_ $excep = $_
Dump-Error -excep $excep -msg "Failed to become user $username" Dump-Error -excep $excep -msg "Failed to become user $username"
} Finally {
Remove-Item $temp -ErrorAction SilentlyContinue
} }
$host.SetShouldExit($rc) $host.SetShouldExit($rc)
} }
@ -1351,275 +1127,81 @@ $ErrorActionPreference = "Stop"
# return asyncresult to controller # return asyncresult to controller
$exec_wrapper = { $exec_wrapper = {
$DebugPreference = "Continue" &chcp.com 65001 > $null
$ErrorActionPreference = "Stop" $DebugPreference = "Continue"
Set-StrictMode -Version 2 $ErrorActionPreference = "Stop"
Set-StrictMode -Version 2
function ConvertTo-HashtableFromPsCustomObject ($myPsObject){ function ConvertTo-HashtableFromPsCustomObject ($myPsObject){
$output = @{}; $output = @{};
$myPsObject | Get-Member -MemberType *Property | % { $myPsObject | Get-Member -MemberType *Property | % {
$val = $myPsObject.($_.name); $val = $myPsObject.($_.name);
If ($val -is [psobject]) { If ($val -is [psobject]) {
$val = ConvertTo-HashtableFromPsCustomObject $val $val = ConvertTo-HashtableFromPsCustomObject $val
}
$output.($_.name) = $val
} }
$output.($_.name) = $val return $output;
} }
return $output;
}
# stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
# exec runspace, capture output, cleanup, return module output
$json_raw = [System.Console]::In.ReadToEnd() # store the pipe name and no. of bytes to read, these are populated by the
# Run function before being run - do not remove or change
$pipe_name = ""
$bytes_length = 0
If (-not $json_raw) { # stream JSON including become_pw, ps_module_payload, bin_module_payload, become_payload, write_payload_path, preserve directives
Write-Error "no input given" -Category InvalidArgument # exec runspace, capture output, cleanup, return module output
} $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()
}
$json_raw = [System.Text.Encoding]::UTF8.GetString($input_bytes)
$payload = ConvertTo-HashtableFromPsCustomObject (ConvertFrom-Json $json_raw) If (-not $json_raw) {
Write-Error "no input given" -Category InvalidArgument
}
# TODO: handle binary modules $payload = ConvertTo-HashtableFromPsCustomObject (ConvertFrom-Json $json_raw)
# TODO: handle persistence
$actions = $payload.actions # TODO: handle binary modules
# TODO: handle persistence
# pop 0th action as entrypoint $actions = $payload.actions
$entrypoint = $payload.($actions[0])
$payload.actions = $payload.actions[1..99]
$entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint)) # pop 0th action as entrypoint
$entrypoint = $payload.($actions[0])
$payload.actions = $payload.actions[1..99]
# load the current action entrypoint as a module custom object with a Run method $entrypoint = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($entrypoint))
$entrypoint = New-Module -ScriptBlock ([scriptblock]::Create($entrypoint)) -AsCustomObject
Set-Variable -Scope global -Name complex_args -Value $payload["module_args"] | Out-Null # load the current action entrypoint as a module custom object with a Run method
$entrypoint = New-Module -ScriptBlock ([scriptblock]::Create($entrypoint)) -AsCustomObject
# dynamically create/load modules Set-Variable -Scope global -Name complex_args -Value $payload["module_args"] | Out-Null
ForEach ($mod in $payload.powershell_modules.GetEnumerator()) {
$decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value))
New-Module -ScriptBlock ([scriptblock]::Create($decoded_module)) -Name $mod.Key | Import-Module -WarningAction SilentlyContinue | Out-Null
}
$output = $entrypoint.Run($payload) # dynamically create/load modules
ForEach ($mod in $payload.powershell_modules.GetEnumerator()) {
$decoded_module = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($mod.Value))
New-Module -ScriptBlock ([scriptblock]::Create($decoded_module)) -Name $mod.Key | Import-Module -WarningAction SilentlyContinue | Out-Null
}
Write-Output $output $output = $entrypoint.Run($payload)
Write-Output $output
} # end exec_wrapper } # end exec_wrapper
Function Run($payload) { Function Run($payload) {
# BEGIN Ansible.Async native type definition
$native_process_util = @"
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace Ansible.Async {
public static class NativeProcessUtil
{
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, BestFitMapping=false)]
public static extern bool CreateProcess(
[MarshalAs(UnmanagedType.LPTStr)]
string lpApplicationName,
StringBuilder lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
[MarshalAs(UnmanagedType.LPTStr)]
string lpCurrentDirectory,
STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
public static extern uint SearchPath (
string lpPath,
string lpFileName,
string lpExtension,
int nBufferLength,
[MarshalAs (UnmanagedType.LPTStr)]
StringBuilder lpBuffer,
out IntPtr lpFilePart);
[DllImport("kernel32.dll")]
public static extern bool CreatePipe(out IntPtr hReadPipe, out IntPtr hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, uint nSize);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr GetStdHandle(StandardHandleValues nStdHandle);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool SetHandleInformation(IntPtr hObject, HandleFlags dwMask, int dwFlags);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref int lpSize);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern bool UpdateProcThreadAttribute(
IntPtr lpAttributeList,
uint dwFlags,
IntPtr Attribute,
IntPtr lpValue,
IntPtr cbSize,
IntPtr lpPreviousValue,
IntPtr lpReturnSize);
public static string SearchPath(string findThis)
{
StringBuilder sbOut = new StringBuilder(1024);
IntPtr filePartOut;
if(SearchPath(null, findThis, null, sbOut.Capacity, sbOut, out filePartOut) == 0)
throw new FileNotFoundException("Couldn't locate " + findThis + " on path");
return sbOut.ToString();
}
[DllImport("kernel32.dll", SetLastError=true)]
static extern SafeFileHandle OpenThread(
ThreadAccessRights dwDesiredAccess,
bool bInheritHandle,
int dwThreadId);
[DllImport("kernel32.dll", SetLastError=true)]
static extern int ResumeThread(SafeHandle hThread);
public static void ResumeThreadById(int threadId)
{
var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId);
if(threadHandle.IsInvalid)
throw new Exception(String.Format("Thread ID {0} is invalid ({1})", threadId,
new Win32Exception(Marshal.GetLastWin32Error()).Message));
try
{
if(ResumeThread(threadHandle) == -1)
throw new Exception(String.Format("Thread ID {0} cannot be resumed ({1})", threadId,
new Win32Exception(Marshal.GetLastWin32Error()).Message));
}
finally
{
threadHandle.Dispose();
}
}
public static void ResumeProcessById(int pid)
{
var proc = Process.GetProcessById(pid);
// wait for at least one suspended thread in the process (this handles possible slow startup race where
// primary thread of created-suspended process has not yet become runnable)
var retryCount = 0;
while(!proc.Threads.OfType<ProcessThread>().Any(t=>t.ThreadState == System.Diagnostics.ThreadState.Wait &&
t.WaitReason == ThreadWaitReason.Suspended))
{
proc.Refresh();
Thread.Sleep(50);
if (retryCount > 100)
throw new InvalidOperationException(String.Format("No threads were suspended in target PID {0} after 5s", pid));
}
foreach(var thread in proc.Threads.OfType<ProcessThread>().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait &&
t.WaitReason == ThreadWaitReason.Suspended))
ResumeThreadById(thread.Id);
}
}
[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;
public Int32 dwX;
public Int32 dwY;
public Int32 dwXSize;
public Int32 dwYSize;
public Int32 dwXCountChars;
public Int32 dwYCountChars;
public Int32 dwFillAttribute;
public Int32 dwFlags;
public Int16 wShowWindow;
public Int16 cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr 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;
}
[Flags]
enum ThreadAccessRights : uint
{
SUSPEND_RESUME = 0x0002
}
[Flags]
public enum StartupInfoFlags : uint
{
USESTDHANDLES = 0x00000100
}
public enum StandardHandleValues : int
{
STD_INPUT_HANDLE = -10,
STD_OUTPUT_HANDLE = -11,
STD_ERROR_HANDLE = -12
}
[Flags]
public enum HandleFlags : uint
{
None = 0,
INHERIT = 1
}
}
"@ # END Ansible.Async native type definition
$original_tmp = $env:TMP
$original_temp = $env:TEMP
$remote_tmp = $payload["module_args"]["_ansible_remote_tmp"] $remote_tmp = $payload["module_args"]["_ansible_remote_tmp"]
$remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp) $remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp)
if ($null -eq $remote_tmp) { if ($null -eq $remote_tmp) {
@ -1636,104 +1218,73 @@ Function Run($payload) {
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) | Out-Null [System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) | Out-Null
$env:TMP = $remote_tmp # can't use anonymous pipes as the spawned process will not be a child due to
$env:TEMP = $remote_tmp # the way WMI works, use a named pipe with a random name instead and set to
Add-Type -TypeDefinition $native_process_util -Debug:$false # only allow current user to read from the pipe
$env:TMP = $original_tmp $pipe_name = "ansible-async-$jid-$([guid]::NewGuid())"
$env:TEMP = $original_temp $current_user = ([Security.Principal.WindowsIdentity]::GetCurrent()).User
# FUTURE: create under new job to ensure all children die on exit?
# FUTURE: move these flags into C# enum?
# start process suspended + breakaway so we can record the watchdog pid without worrying about a completion race
Set-Variable CREATE_BREAKAWAY_FROM_JOB -Value ([uint32]0x01000000) -Option Constant
Set-Variable CREATE_SUSPENDED -Value ([uint32]0x00000004) -Option Constant
Set-Variable CREATE_UNICODE_ENVIRONMENT -Value ([uint32]0x000000400) -Option Constant
Set-Variable CREATE_NEW_CONSOLE -Value ([uint32]0x00000010) -Option Constant
Set-Variable EXTENDED_STARTUPINFO_PRESENT -Value ([uint32]0x00080000) -Option Constant
$pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB -bor $CREATE_UNICODE_ENVIRONMENT -bor $CREATE_NEW_CONSOLE `
-bor $CREATE_SUSPENDED -bor $EXTENDED_STARTUPINFO_PRESENT
# execute the dynamic watchdog as a breakway process to free us from the WinRM job, which will in turn exec the module
$si = New-Object Ansible.Async.STARTUPINFOEX
# setup stdin redirection, we'll leave stdout/stderr as normal
$si.startupInfo.dwFlags = [Ansible.Async.StartupInfoFlags]::USESTDHANDLES
$si.startupInfo.hStdOutput = [Ansible.Async.NativeProcessUtil]::GetStdHandle([Ansible.Async.StandardHandleValues]::STD_OUTPUT_HANDLE)
$si.startupInfo.hStdError = [Ansible.Async.NativeProcessUtil]::GetStdHandle([Ansible.Async.StandardHandleValues]::STD_ERROR_HANDLE)
$stdin_read = $stdin_write = 0
$pipesec = New-Object Ansible.Async.SECURITY_ATTRIBUTES
$pipesec.bInheritHandle = $true
If(-not [Ansible.Async.NativeProcessUtil]::CreatePipe([ref]$stdin_read, [ref]$stdin_write, $pipesec, 0)) {
throw "Stdin pipe setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
If(-not [Ansible.Async.NativeProcessUtil]::SetHandleInformation($stdin_write, [Ansible.Async.HandleFlags]::INHERIT, 0)) {
throw "Stdin handle setup failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
$si.startupInfo.hStdInput = $stdin_read
# create an attribute list with our explicit handle inheritance list to pass to CreateProcess
[int]$buf_sz = 0
# determine the buffer size necessary for our attribute list
If(-not [Ansible.Async.NativeProcessUtil]::InitializeProcThreadAttributeList([IntPtr]::Zero, 1, 0, [ref]$buf_sz)) {
$last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
If($last_err -ne 122) { # ERROR_INSUFFICIENT_BUFFER
throw "Attribute list size query failed, Win32Error: $last_err"
}
}
$si.lpAttributeList = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buf_sz)
# initialize the attribute list
If(-not [Ansible.Async.NativeProcessUtil]::InitializeProcThreadAttributeList($si.lpAttributeList, 1, 0, [ref]$buf_sz)) {
throw "Attribute list init failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
$handles_to_inherit = [IntPtr[]]@($stdin_read)
$pinned_handles = [System.Runtime.InteropServices.GCHandle]::Alloc($handles_to_inherit, [System.Runtime.InteropServices.GCHandleType]::Pinned)
# update the attribute list with the handles we want to inherit
If(-not [Ansible.Async.NativeProcessUtil]::UpdateProcThreadAttribute($si.lpAttributeList, 0, 0x20002 <# PROC_THREAD_ATTRIBUTE_HANDLE_LIST #>, `
$pinned_handles.AddrOfPinnedObject(), [System.Runtime.InteropServices.Marshal]::SizeOf([type][IntPtr]) * $handles_to_inherit.Length, `
[System.IntPtr]::Zero, [System.IntPtr]::Zero)) {
throw "Attribute list update failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
# need to use a preamble-free version of UTF8Encoding
$utf8_encoding = New-Object System.Text.UTF8Encoding @($false)
$stdin_fs = New-Object System.IO.FileStream @($stdin_write, [System.IO.FileAccess]::Write, $true, 32768)
$stdin = New-Object System.IO.StreamWriter @($stdin_fs, $utf8_encoding, 32768)
$pi = New-Object Ansible.Async.PROCESS_INFORMATION
$encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper.ToString()))
# FUTURE: direct cmdline CreateProcess path lookup fails- this works but is sub-optimal
$exec_cmd = [Ansible.Async.NativeProcessUtil]::SearchPath("powershell.exe")
$exec_args = New-Object System.Text.StringBuilder @("`"$exec_cmd`" -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command")
# TODO: use proper Win32Exception + error
If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args,
[IntPtr]::Zero, [IntPtr]::Zero, $true, $pstartup_flags, [IntPtr]::Zero, $env:windir, $si, [ref]$pi)) {
#throw New-Object System.ComponentModel.Win32Exception
throw "Worker creation failed, Win32Error: $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())"
}
# FUTURE: watch process for quick exit, capture stdout/stderr and return failure
$watchdog_pid = $pi.dwProcessId
[Ansible.Async.NativeProcessUtil]::ResumeProcessById($watchdog_pid)
# once process is resumed, we can send payload over stdin
$payload_string = $payload | ConvertTo-Json -Depth 99 -Compress $payload_string = $payload | ConvertTo-Json -Depth 99 -Compress
$stdin.WriteLine($payload_string) $payload_bytes = [System.Text.Encoding]::UTF8.GetBytes($payload_string)
$stdin.Close()
$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)
$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 {
$exec_wrapper_str = $exec_wrapper.ToString()
$exec_wrapper_str = $exec_wrapper_str.Replace('$pipe_name = ""', "`$pipe_name = `"$pipe_name`"")
$exec_wrapper_str = $exec_wrapper_str.Replace('$bytes_length = 0', "`$bytes_length = $($payload_bytes.Count)")
$encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($exec_wrapper_str))
$exec_args = "powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command"
# not all connection plugins support breakaway from job that is required
# for async, Win32_Process.Create() is still able to escape so we use
# that here
$process = Invoke-CimMethod -ClassName Win32_Process -Name Create -Arguments @{CommandLine=$exec_args}
$rc = $process.ReturnValue
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
# wait until the client connects, throw an error if the timeout is reached
$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 the exec manifest to the child process
$pipe.Write($payload_bytes, 0, $payload_bytes.Count)
$pipe.Flush()
$pipe.WaitForPipeDrain()
} finally {
$pipe.Close()
}
# populate initial results before we resume the process to avoid result race # populate initial results before we resume the process to avoid result race
$result = @{ $result = @{

View file

@ -22,7 +22,7 @@
- name: async poll immediate success - name: async poll immediate success
async_test: async_test:
sleep_delay_sec: 0 sleep_delay_sec: 0
async: 10 async: 20
poll: 1 poll: 1
register: asyncresult register: asyncresult
@ -59,7 +59,7 @@
- name: async poll retry - name: async poll retry
async_test: async_test:
sleep_delay_sec: 5 sleep_delay_sec: 5
async: 10 async: 20
poll: 1 poll: 1
register: asyncresult register: asyncresult
@ -94,8 +94,8 @@
- name: async poll timeout - name: async poll timeout
async_test: async_test:
sleep_delay_sec: 5 sleep_delay_sec: 25
async: 3 async: 20
poll: 1 poll: 1
register: asyncresult register: asyncresult
ignore_errors: true ignore_errors: true
@ -146,7 +146,7 @@
- name: echo some non ascii characters - name: echo some non ascii characters
win_command: cmd.exe /c echo über den Fußgängerübergang gehen win_command: cmd.exe /c echo über den Fußgängerübergang gehen
async: 10 async: 20
poll: 1 poll: 1
register: nonascii_output register: nonascii_output

View file

@ -189,37 +189,16 @@
- whoami_out.label.sid == 'S-1-16-16384' - whoami_out.label.sid == 'S-1-16-16384'
- whoami_out.logon_type == 'Service' - whoami_out.logon_type == 'Service'
# Test out Async on Windows Server 2012+ - name: test become + async
- name: get OS version
win_shell: |
$version = [System.Environment]::OSVersion.Version
if ($version -ge [Version]"6.2") {
"async"
} elseif ($version -lt [Version]"6.1") {
"old-gramps"
} else {
""
}
register: os_version
- name: test become + async on older hosts
vars: *become_vars vars: *become_vars
win_command: whoami win_command: whoami
async: 10 async: 10
register: whoami_out register: whoami_out
ignore_errors: yes
- name: verify older hosts failed with become + async - name: verify become + async worked
assert:
that:
- whoami_out is failed
when: os_version.stdout_lines[0] != "async"
- name: verify newer hosts worked with become + async
assert: assert:
that: that:
- whoami_out is successful - whoami_out is successful
when: os_version.stdout_lines[0] == "async"
- name: test failure with string become invalid key - name: test failure with string become invalid key
vars: *become_vars vars: *become_vars
@ -244,12 +223,24 @@
failed_when: "failed_flags_invalid_flag.msg != \"Failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\"" failed_when: "failed_flags_invalid_flag.msg != \"Failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\""
# Server 2008 doesn't work with network and network_cleartext, there isn't really a reason why you would want this anyway # Server 2008 doesn't work with network and network_cleartext, there isn't really a reason why you would want this anyway
- name: check if we are running on a dinosaur, neanderthal or an OS of the modern age
win_shell: |
$version = [System.Environment]::OSVersion.Version
if ($version -lt [Version]"6.1") {
"dinosaur"
} elseif ($version -lt [Version]"6.2") {
"neanderthal"
} else {
"False"
}
register: os_version
- name: become different types - name: become different types
vars: *become_vars vars: *become_vars
win_whoami: win_whoami:
become_flags: logon_type={{item.type}} become_flags: logon_type={{item.type}}
register: become_logon_type register: become_logon_type
when: not ((item.type == 'network' or item.type == 'network_cleartext') and os_version.stdout_lines[0] == "old-gramps") when: not ((item.type == 'network' or item.type == 'network_cleartext') and os_version.stdout_lines[0] == "dinosaur")
failed_when: become_logon_type.logon_type != item.actual and become_logon_type.sid != user_limited_result.sid failed_when: become_logon_type.logon_type != item.actual and become_logon_type.sid != user_limited_result.sid
with_items: with_items:
- type: interactive - type: interactive
@ -298,6 +289,7 @@
become_flags: logon_flags={{item.flags}} become_flags: logon_flags={{item.flags}}
register: become_logon_flags register: become_logon_flags
failed_when: become_logon_flags.stdout_lines[0]|bool != item.actual failed_when: become_logon_flags.stdout_lines[0]|bool != item.actual
when: os_version.stdout_lines[0] not in ["dinosaur", "neanderthal"] # usual suspect 2008 doesn't support the no profile flags
with_items: with_items:
- flags: - flags:
actual: False actual: False