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:
parent
57ea4cafff
commit
2af36412f9
5 changed files with 186 additions and 640 deletions
3
changelogs/fragments/win_become_async_older_hosts.yml
Normal file
3
changelogs/fragments/win_become_async_older_hosts.yml
Normal 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
|
|
@ -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
|
||||||
|
|
|
@ -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 = @{
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue