mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
win become: refactor and add support for passwordless become (#48082)
* win become: refactor and add support for passwordless become * make tests more stable * fix up dep message for Load-CommandUtils * Add further check for System impersonation token * re-add support for become with accounts that have no password * doc fixes and slight code improvements * fix doc sanity issue
This commit is contained in:
parent
b3ac5b637a
commit
190d1ed7f1
13 changed files with 2586 additions and 1105 deletions
3
changelogs/fragments/win_become-passwordless.yaml
Normal file
3
changelogs/fragments/win_become-passwordless.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
minor_changes:
|
||||||
|
- windows become - Add support for passwordless become.
|
||||||
|
- windows become - Moved to shared C# util so modules can utilise the code.
|
|
@ -523,6 +523,42 @@ Because local service accounts do not have passwords, the
|
||||||
``ansible_become_password`` parameter is not required and is ignored if
|
``ansible_become_password`` parameter is not required and is ignored if
|
||||||
specified.
|
specified.
|
||||||
|
|
||||||
|
Become without setting a Password
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
As of Ansible 2.8, ``become`` can be used to become a local or domain account
|
||||||
|
without requiring a password for that account. For this method to work, the
|
||||||
|
following requirements must be met:
|
||||||
|
|
||||||
|
* The connection user has the ``SeDebugPrivilege`` privilege assigned
|
||||||
|
* The connection user is part of the ``BUILTIN\Administrators`` group
|
||||||
|
* The ``become_user`` has either the ``SeBatchLogonRight`` or ``SeNetworkLogonRight`` user right
|
||||||
|
|
||||||
|
Using become without a password is achieved in one of two different methods:
|
||||||
|
|
||||||
|
* Duplicating an existing logon session's token if the account is already logged on
|
||||||
|
* Using S4U to generate a logon token that is valid on the remote host only
|
||||||
|
|
||||||
|
In the first scenario, the become process is spawned from another logon of that
|
||||||
|
user account. This could be an existing RDP logon, console logon, but this is
|
||||||
|
not guaranteed to occur all the time. This is similar to the
|
||||||
|
``Run only when user is logged on`` option for a Scheduled Task.
|
||||||
|
|
||||||
|
In the case where another logon of the become account does not exist, S4U is
|
||||||
|
used to create a new logon and run the module through that. This is similar to
|
||||||
|
the ``Run whether user is logged on or not`` with the ``Do not store password``
|
||||||
|
option for a Scheduled Task. In this scenario, the become process will not be
|
||||||
|
able to access any network resources like a normal WinRM process.
|
||||||
|
|
||||||
|
To make a distinction between using become with no password and becoming an
|
||||||
|
account that has no password make sure to keep ``ansible_become_pass`` as
|
||||||
|
undefined or set ``ansible_become_pass:``.
|
||||||
|
|
||||||
|
.. Note:: Because there are no guarantees an existing token will exist for a
|
||||||
|
user when Ansible runs, there's a high change the become process will only
|
||||||
|
have access to local resources. Use become with a password if the task needs
|
||||||
|
to access network resources
|
||||||
|
|
||||||
Accounts without a Password
|
Accounts without a Password
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
@ -530,8 +566,7 @@ Accounts without a Password
|
||||||
|
|
||||||
Ansible can be used to become an account that does not have a password (like the
|
Ansible can be used to become an account that does not have a password (like the
|
||||||
``Guest`` account). To become an account without a password, set up the
|
``Guest`` account). To become an account without a password, set up the
|
||||||
variables like normal but either do not define ``ansible_become_pass`` or set
|
variables like normal but set ``ansible_become_pass: ''``.
|
||||||
``ansible_become_pass: ''``.
|
|
||||||
|
|
||||||
Before become can work on an account like this, the local policy
|
Before become can work on an account like this, the local policy
|
||||||
`Accounts: Limit local account use of blank passwords to console logon only <https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/jj852174(v=ws.11)>`_
|
`Accounts: Limit local account use of blank passwords to console logon only <https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/jj852174(v=ws.11)>`_
|
||||||
|
|
|
@ -5,6 +5,7 @@ param(
|
||||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
|
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.AddType
|
||||||
#AnsibleRequires -CSharpUtil Ansible.Become
|
#AnsibleRequires -CSharpUtil Ansible.Become
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
@ -74,18 +75,24 @@ Function Get-BecomeFlags($flags) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-AnsibleLog "INFO - loading C# become code" "become_wrapper"
|
Write-AnsibleLog "INFO - loading C# become code" "become_wrapper"
|
||||||
$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"]))
|
$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
|
||||||
|
|
||||||
# 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"])
|
$new_tmp = [System.Environment]::ExpandEnvironmentVariables($Payload.module_args["_ansible_remote_tmp"])
|
||||||
$old_tmp = $env:TMP
|
$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"]))
|
||||||
$env:TMP = $new_tmp
|
$process_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Process"]))
|
||||||
Add-Type -TypeDefinition $become_def -Debug:$false
|
Add-CSharpType -References $become_def, $process_def -TempPath $new_tmp -IncludeDebugInfo
|
||||||
$env:TMP = $old_tmp
|
|
||||||
|
|
||||||
$username = $Payload.become_user
|
$username = $Payload.become_user
|
||||||
$password = $Payload.become_password
|
$password = $Payload.become_password
|
||||||
|
# We need to set password to the value of NullString so a null password is preserved when crossing the .NET
|
||||||
|
# boundary. If we pass $null it will automatically be converted to "" and we need to keep the distinction for
|
||||||
|
# accounts that don't have a password and when someone wants to become without knowing the password.
|
||||||
|
if ($null -eq $password) {
|
||||||
|
$password = [NullString]::Value
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$logon_type, $logon_flags = Get-BecomeFlags -flags $Payload.become_flags
|
$logon_type, $logon_flags = Get-BecomeFlags -flags $Payload.become_flags
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -109,7 +116,7 @@ $bootstrap_wrapper = {
|
||||||
&$exec_wrapper
|
&$exec_wrapper
|
||||||
}
|
}
|
||||||
$exec_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper.ToString()))
|
$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_command_line = "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?
|
$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
|
# pop the become_wrapper action so we don't get stuck in a loop
|
||||||
|
@ -124,8 +131,8 @@ $exec_wrapper += "`0`0`0`0" + $payload_json
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Write-AnsibleLog "INFO - starting become process '$lp_command_line'" "become_wrapper"
|
Write-AnsibleLog "INFO - starting become process '$lp_command_line'" "become_wrapper"
|
||||||
$result = [Ansible.Become.BecomeUtil]::RunAsUser($username, $password, $lp_command_line,
|
$result = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($username, $password, $logon_flags, $logon_type,
|
||||||
$lp_current_directory, $exec_wrapper, $logon_flags, $logon_type)
|
$null, $lp_command_line, $lp_current_directory, $null, $exec_wrapper)
|
||||||
Write-AnsibleLog "INFO - become process complete with rc: $($result.ExitCode)" "become_wrapper"
|
Write-AnsibleLog "INFO - become process complete with rc: $($result.ExitCode)" "become_wrapper"
|
||||||
$stdout = $result.StandardOut
|
$stdout = $result.StandardOut
|
||||||
try {
|
try {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
445
lib/ansible/module_utils/csharp/Ansible.Process.cs
Normal file
445
lib/ansible/module_utils/csharp/Ansible.Process.cs
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.ConstrainedExecution;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Ansible.Process
|
||||||
|
{
|
||||||
|
internal class NativeHelpers
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public class SECURITY_ATTRIBUTES
|
||||||
|
{
|
||||||
|
public UInt32 nLength;
|
||||||
|
public IntPtr lpSecurityDescriptor;
|
||||||
|
public bool bInheritHandle = false;
|
||||||
|
public SECURITY_ATTRIBUTES()
|
||||||
|
{
|
||||||
|
nLength = (UInt32)Marshal.SizeOf(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public class STARTUPINFO
|
||||||
|
{
|
||||||
|
public UInt32 cb;
|
||||||
|
public IntPtr lpReserved;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpDesktop;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpTitle;
|
||||||
|
public UInt32 dwX;
|
||||||
|
public UInt32 dwY;
|
||||||
|
public UInt32 dwXSize;
|
||||||
|
public UInt32 dwYSize;
|
||||||
|
public UInt32 dwXCountChars;
|
||||||
|
public UInt32 dwYCountChars;
|
||||||
|
public UInt32 dwFillAttribute;
|
||||||
|
public StartupInfoFlags dwFlags;
|
||||||
|
public UInt16 wShowWindow;
|
||||||
|
public UInt16 cbReserved2;
|
||||||
|
public IntPtr lpReserved2;
|
||||||
|
public SafeFileHandle hStdInput;
|
||||||
|
public SafeFileHandle hStdOutput;
|
||||||
|
public SafeFileHandle hStdError;
|
||||||
|
public STARTUPINFO()
|
||||||
|
{
|
||||||
|
cb = (UInt32)Marshal.SizeOf(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public class STARTUPINFOEX
|
||||||
|
{
|
||||||
|
public STARTUPINFO startupInfo;
|
||||||
|
public IntPtr lpAttributeList;
|
||||||
|
public STARTUPINFOEX()
|
||||||
|
{
|
||||||
|
startupInfo = new STARTUPINFO();
|
||||||
|
startupInfo.cb = (UInt32)Marshal.SizeOf(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct PROCESS_INFORMATION
|
||||||
|
{
|
||||||
|
public IntPtr hProcess;
|
||||||
|
public IntPtr hThread;
|
||||||
|
public int dwProcessId;
|
||||||
|
public int dwThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum ProcessCreationFlags : uint
|
||||||
|
{
|
||||||
|
CREATE_NEW_CONSOLE = 0x00000010,
|
||||||
|
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
|
||||||
|
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum StartupInfoFlags : uint
|
||||||
|
{
|
||||||
|
USESTDHANDLES = 0x00000100
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum HandleFlags : uint
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
INHERIT = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NativeMethods
|
||||||
|
{
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool AllocConsole();
|
||||||
|
|
||||||
|
[DllImport("shell32.dll", SetLastError = true)]
|
||||||
|
public static extern SafeMemoryBuffer CommandLineToArgvW(
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
|
||||||
|
out int pNumArgs);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool CreatePipe(
|
||||||
|
out SafeFileHandle hReadPipe,
|
||||||
|
out SafeFileHandle hWritePipe,
|
||||||
|
NativeHelpers.SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||||
|
UInt32 nSize);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern bool CreateProcessW(
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpApplicationName,
|
||||||
|
StringBuilder lpCommandLine,
|
||||||
|
IntPtr lpProcessAttributes,
|
||||||
|
IntPtr lpThreadAttributes,
|
||||||
|
bool bInheritHandles,
|
||||||
|
NativeHelpers.ProcessCreationFlags dwCreationFlags,
|
||||||
|
SafeMemoryBuffer lpEnvironment,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpCurrentDirectory,
|
||||||
|
NativeHelpers.STARTUPINFOEX lpStartupInfo,
|
||||||
|
out NativeHelpers.PROCESS_INFORMATION lpProcessInformation);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool FreeConsole();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GetConsoleWindow();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool GetExitCodeProcess(
|
||||||
|
SafeWaitHandle hProcess,
|
||||||
|
out UInt32 lpExitCode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern uint SearchPathW(
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpPath,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpExtension,
|
||||||
|
UInt32 nBufferLength,
|
||||||
|
[MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpBuffer,
|
||||||
|
out IntPtr lpFilePart);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetConsoleCP(
|
||||||
|
UInt32 wCodePageID);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetConsoleOutputCP(
|
||||||
|
UInt32 wCodePageID);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetHandleInformation(
|
||||||
|
SafeFileHandle hObject,
|
||||||
|
NativeHelpers.HandleFlags dwMask,
|
||||||
|
NativeHelpers.HandleFlags dwFlags);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern UInt32 WaitForSingleObject(
|
||||||
|
SafeWaitHandle hHandle,
|
||||||
|
UInt32 dwMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
|
||||||
|
{
|
||||||
|
public SafeMemoryBuffer() : base(true) { }
|
||||||
|
public SafeMemoryBuffer(int cb) : base(true)
|
||||||
|
{
|
||||||
|
base.SetHandle(Marshal.AllocHGlobal(cb));
|
||||||
|
}
|
||||||
|
public SafeMemoryBuffer(IntPtr handle) : base(true)
|
||||||
|
{
|
||||||
|
base.SetHandle(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
|
||||||
|
protected override bool ReleaseHandle()
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Result
|
||||||
|
{
|
||||||
|
public string StandardOut { get; internal set; }
|
||||||
|
public string StandardError { get; internal set; }
|
||||||
|
public uint ExitCode { get; internal set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProcessUtil
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a command line string into an argv array according to the Windows rules
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lpCommandLine">The command line to parse</param>
|
||||||
|
/// <returns>An array of arguments interpreted by Windows</returns>
|
||||||
|
public static string[] ParseCommandLine(string lpCommandLine)
|
||||||
|
{
|
||||||
|
int numArgs;
|
||||||
|
using (SafeMemoryBuffer buf = NativeMethods.CommandLineToArgvW(lpCommandLine, out numArgs))
|
||||||
|
{
|
||||||
|
if (buf.IsInvalid)
|
||||||
|
throw new Win32Exception("Error parsing command line");
|
||||||
|
IntPtr[] strptrs = new IntPtr[numArgs];
|
||||||
|
Marshal.Copy(buf.DangerousGetHandle(), strptrs, 0, numArgs);
|
||||||
|
return strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches the path for the executable specified. Will throw a Win32Exception if the file is not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lpFileName">The executable to search for</param>
|
||||||
|
/// <returns>The full path of the executable to search for</returns>
|
||||||
|
public static string SearchPath(string lpFileName)
|
||||||
|
{
|
||||||
|
StringBuilder sbOut = new StringBuilder(0);
|
||||||
|
IntPtr filePartOut = IntPtr.Zero;
|
||||||
|
UInt32 res = NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut);
|
||||||
|
if (res == 0)
|
||||||
|
{
|
||||||
|
int lastErr = Marshal.GetLastWin32Error();
|
||||||
|
if (lastErr == 2) // ERROR_FILE_NOT_FOUND
|
||||||
|
throw new FileNotFoundException(String.Format("Could not find file '{0}'.", lpFileName));
|
||||||
|
else
|
||||||
|
throw new Win32Exception(String.Format("SearchPathW({0}) failed to get buffer length", lpFileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
sbOut.EnsureCapacity((int)res);
|
||||||
|
if (NativeMethods.SearchPathW(null, lpFileName, null, (UInt32)sbOut.Capacity, sbOut, out filePartOut) == 0)
|
||||||
|
throw new Win32Exception(String.Format("SearchPathW({0}) failed", lpFileName));
|
||||||
|
|
||||||
|
return sbOut.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result CreateProcess(string command)
|
||||||
|
{
|
||||||
|
return CreateProcess(null, command, null, null, String.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||||
|
IDictionary environment)
|
||||||
|
{
|
||||||
|
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, String.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||||
|
IDictionary environment, string stdin)
|
||||||
|
{
|
||||||
|
byte[] stdinBytes;
|
||||||
|
if (String.IsNullOrEmpty(stdin))
|
||||||
|
stdinBytes = new byte[0];
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!stdin.EndsWith(Environment.NewLine))
|
||||||
|
stdin += Environment.NewLine;
|
||||||
|
stdinBytes = new UTF8Encoding(false).GetBytes(stdin);
|
||||||
|
}
|
||||||
|
return CreateProcess(lpApplicationName, lpCommandLine, lpCurrentDirectory, environment, stdinBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a process based on the CreateProcess API call.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lpApplicationName">The name of the executable or batch file to execute</param>
|
||||||
|
/// <param name="lpCommandLine">The command line to execute, typically this includes lpApplication as the first argument</param>
|
||||||
|
/// <param name="lpCurrentDirectory">The full path to the current directory for the process, null will have the same cwd as the calling process</param>
|
||||||
|
/// <param name="environment">A dictionary of key/value pairs to define the new process environment</param>
|
||||||
|
/// <param name="stdin">A byte array to send over the stdin pipe</param>
|
||||||
|
/// <returns>Result object that contains the command output and return code</returns>
|
||||||
|
public static Result CreateProcess(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory,
|
||||||
|
IDictionary environment, byte[] stdin)
|
||||||
|
{
|
||||||
|
NativeHelpers.ProcessCreationFlags creationFlags = NativeHelpers.ProcessCreationFlags.CREATE_UNICODE_ENVIRONMENT |
|
||||||
|
NativeHelpers.ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT;
|
||||||
|
NativeHelpers.PROCESS_INFORMATION pi = new NativeHelpers.PROCESS_INFORMATION();
|
||||||
|
NativeHelpers.STARTUPINFOEX si = new NativeHelpers.STARTUPINFOEX();
|
||||||
|
si.startupInfo.dwFlags = NativeHelpers.StartupInfoFlags.USESTDHANDLES;
|
||||||
|
|
||||||
|
SafeFileHandle stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinRead, stdinWrite;
|
||||||
|
CreateStdioPipes(si, out stdoutRead, out stdoutWrite, out stderrRead, out stderrWrite, out stdinRead,
|
||||||
|
out stdinWrite);
|
||||||
|
FileStream stdinStream = new FileStream(stdinWrite, FileAccess.Write);
|
||||||
|
|
||||||
|
// $null from PowerShell ends up as an empty string, we need to convert back as an empty string doesn't
|
||||||
|
// make sense for these parameters
|
||||||
|
if (lpApplicationName == "")
|
||||||
|
lpApplicationName = null;
|
||||||
|
|
||||||
|
if (lpCurrentDirectory == "")
|
||||||
|
lpCurrentDirectory = null;
|
||||||
|
|
||||||
|
using (SafeMemoryBuffer lpEnvironment = CreateEnvironmentPointer(environment))
|
||||||
|
{
|
||||||
|
// Create console with utf-8 CP if no existing console is present
|
||||||
|
bool isConsole = false;
|
||||||
|
if (NativeMethods.GetConsoleWindow() == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
isConsole = NativeMethods.AllocConsole();
|
||||||
|
|
||||||
|
// Set console input/output codepage to UTF-8
|
||||||
|
NativeMethods.SetConsoleCP(65001);
|
||||||
|
NativeMethods.SetConsoleOutputCP(65001);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StringBuilder commandLine = new StringBuilder(lpCommandLine);
|
||||||
|
if (!NativeMethods.CreateProcessW(lpApplicationName, commandLine, IntPtr.Zero, IntPtr.Zero,
|
||||||
|
true, creationFlags, lpEnvironment, lpCurrentDirectory, si, out pi))
|
||||||
|
{
|
||||||
|
throw new Win32Exception("CreateProcessW() failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (isConsole)
|
||||||
|
NativeMethods.FreeConsole();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return WaitProcess(stdoutRead, stdoutWrite, stderrRead, stderrWrite, stdinStream, stdin, pi.hProcess);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void CreateStdioPipes(NativeHelpers.STARTUPINFOEX si, out SafeFileHandle stdoutRead,
|
||||||
|
out SafeFileHandle stdoutWrite, out SafeFileHandle stderrRead, out SafeFileHandle stderrWrite,
|
||||||
|
out SafeFileHandle stdinRead, out SafeFileHandle stdinWrite)
|
||||||
|
{
|
||||||
|
NativeHelpers.SECURITY_ATTRIBUTES pipesec = new NativeHelpers.SECURITY_ATTRIBUTES();
|
||||||
|
pipesec.bInheritHandle = true;
|
||||||
|
|
||||||
|
if (!NativeMethods.CreatePipe(out stdoutRead, out stdoutWrite, pipesec, 0))
|
||||||
|
throw new Win32Exception("STDOUT pipe setup failed");
|
||||||
|
if (!NativeMethods.SetHandleInformation(stdoutRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||||
|
throw new Win32Exception("STDOUT pipe handle setup failed");
|
||||||
|
|
||||||
|
if (!NativeMethods.CreatePipe(out stderrRead, out stderrWrite, pipesec, 0))
|
||||||
|
throw new Win32Exception("STDERR pipe setup failed");
|
||||||
|
if (!NativeMethods.SetHandleInformation(stderrRead, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||||
|
throw new Win32Exception("STDERR pipe handle setup failed");
|
||||||
|
|
||||||
|
if (!NativeMethods.CreatePipe(out stdinRead, out stdinWrite, pipesec, 0))
|
||||||
|
throw new Win32Exception("STDIN pipe setup failed");
|
||||||
|
if (!NativeMethods.SetHandleInformation(stdinWrite, NativeHelpers.HandleFlags.INHERIT, 0))
|
||||||
|
throw new Win32Exception("STDIN pipe handle setup failed");
|
||||||
|
|
||||||
|
si.startupInfo.hStdOutput = stdoutWrite;
|
||||||
|
si.startupInfo.hStdError = stderrWrite;
|
||||||
|
si.startupInfo.hStdInput = stdinRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static SafeMemoryBuffer CreateEnvironmentPointer(IDictionary environment)
|
||||||
|
{
|
||||||
|
IntPtr lpEnvironment = IntPtr.Zero;
|
||||||
|
if (environment != null && environment.Count > 0)
|
||||||
|
{
|
||||||
|
StringBuilder environmentString = new StringBuilder();
|
||||||
|
foreach (DictionaryEntry kv in environment)
|
||||||
|
environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
|
||||||
|
environmentString.Append('\0');
|
||||||
|
|
||||||
|
lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
|
||||||
|
}
|
||||||
|
return new SafeMemoryBuffer(lpEnvironment);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
|
||||||
|
SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess)
|
||||||
|
{
|
||||||
|
// Setup the output buffers and get stdout/stderr
|
||||||
|
UTF8Encoding utf8Encoding = new UTF8Encoding(false);
|
||||||
|
FileStream stdoutFS = new FileStream(stdoutRead, FileAccess.Read, 4096);
|
||||||
|
StreamReader stdout = new StreamReader(stdoutFS, utf8Encoding, true, 4096);
|
||||||
|
stdoutWrite.Close();
|
||||||
|
|
||||||
|
FileStream stderrFS = new FileStream(stderrRead, FileAccess.Read, 4096);
|
||||||
|
StreamReader stderr = new StreamReader(stderrFS, utf8Encoding, true, 4096);
|
||||||
|
stderrWrite.Close();
|
||||||
|
|
||||||
|
stdinStream.Write(stdin, 0, stdin.Length);
|
||||||
|
stdinStream.Close();
|
||||||
|
|
||||||
|
string stdoutStr, stderrStr = null;
|
||||||
|
GetProcessOutput(stdout, stderr, out stdoutStr, out stderrStr);
|
||||||
|
UInt32 rc = GetProcessExitCode(hProcess);
|
||||||
|
|
||||||
|
return new Result
|
||||||
|
{
|
||||||
|
StandardOut = stdoutStr,
|
||||||
|
StandardError = stderrStr,
|
||||||
|
ExitCode = rc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static UInt32 GetProcessExitCode(IntPtr processHandle)
|
||||||
|
{
|
||||||
|
SafeWaitHandle hProcess = new SafeWaitHandle(processHandle, true);
|
||||||
|
NativeMethods.WaitForSingleObject(hProcess, 0xFFFFFFFF);
|
||||||
|
|
||||||
|
UInt32 exitCode;
|
||||||
|
if (!NativeMethods.GetExitCodeProcess(hProcess, out exitCode))
|
||||||
|
throw new Win32Exception("GetExitCodeProcess() failed");
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,393 +1,43 @@
|
||||||
# Copyright (c) 2017 Ansible Project
|
# Copyright (c) 2017 Ansible Project
|
||||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||||
|
|
||||||
$process_util = @"
|
#AnsibleRequires -CSharpUtil Ansible.Process
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Ansible
|
|
||||||
{
|
|
||||||
[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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Flags]
|
|
||||||
public enum StartupInfoFlags : uint
|
|
||||||
{
|
|
||||||
USESTDHANDLES = 0x00000100
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum HandleFlags : uint
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
INHERIT = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
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 CommandUtil
|
|
||||||
{
|
|
||||||
private static UInt32 CREATE_UNICODE_ENVIRONMENT = 0x000000400;
|
|
||||||
private static UInt32 EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)]
|
|
||||||
public static extern bool CreateProcess(
|
|
||||||
[MarshalAs(UnmanagedType.LPWStr)]
|
|
||||||
string lpApplicationName,
|
|
||||||
StringBuilder lpCommandLine,
|
|
||||||
IntPtr lpProcessAttributes,
|
|
||||||
IntPtr lpThreadAttributes,
|
|
||||||
bool bInheritHandles,
|
|
||||||
uint dwCreationFlags,
|
|
||||||
IntPtr lpEnvironment,
|
|
||||||
[MarshalAs(UnmanagedType.LPWStr)]
|
|
||||||
string lpCurrentDirectory,
|
|
||||||
STARTUPINFOEX lpStartupInfo,
|
|
||||||
out PROCESS_INFORMATION lpProcessInformation);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
|
||||||
public static extern bool CreatePipe(
|
|
||||||
out SafeFileHandle hReadPipe,
|
|
||||||
out SafeFileHandle hWritePipe,
|
|
||||||
SECURITY_ATTRIBUTES lpPipeAttributes,
|
|
||||||
uint nSize);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
public 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, 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", SetLastError = true)]
|
|
||||||
static extern IntPtr GetConsoleWindow();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
static extern bool AllocConsole();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
static extern bool FreeConsole();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
static extern bool SetConsoleCP(
|
|
||||||
UInt32 wCodePageID);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
static extern bool SetConsoleOutputCP(
|
|
||||||
UInt32 wCodePageID);
|
|
||||||
|
|
||||||
[DllImport("shell32.dll", SetLastError = true)]
|
|
||||||
static extern IntPtr CommandLineToArgvW(
|
|
||||||
[MarshalAs(UnmanagedType.LPWStr)]
|
|
||||||
string lpCmdLine,
|
|
||||||
out int pNumArgs);
|
|
||||||
|
|
||||||
public static string[] ParseCommandLine(string lpCommandLine)
|
|
||||||
{
|
|
||||||
int numArgs;
|
|
||||||
IntPtr ret = CommandLineToArgvW(lpCommandLine, out numArgs);
|
|
||||||
|
|
||||||
if (ret == IntPtr.Zero)
|
|
||||||
throw new Win32Exception("Error parsing command line");
|
|
||||||
|
|
||||||
IntPtr[] strptrs = new IntPtr[numArgs];
|
|
||||||
Marshal.Copy(ret, strptrs, 0, numArgs);
|
|
||||||
string[] cmdlineParts = strptrs.Select(s => Marshal.PtrToStringUni(s)).ToArray();
|
|
||||||
|
|
||||||
Marshal.FreeHGlobal(ret);
|
|
||||||
|
|
||||||
return cmdlineParts;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string SearchPath(string lpFileName)
|
|
||||||
{
|
|
||||||
StringBuilder sbOut = new StringBuilder(1024);
|
|
||||||
IntPtr filePartOut;
|
|
||||||
|
|
||||||
if (SearchPath(null, lpFileName, null, sbOut.Capacity, sbOut, out filePartOut) == 0)
|
|
||||||
throw new FileNotFoundException(String.Format("Could not locate the following executable {0}", lpFileName));
|
|
||||||
|
|
||||||
return sbOut.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CommandResult
|
|
||||||
{
|
|
||||||
public string StandardOut { get; internal set; }
|
|
||||||
public string StandardError { get; internal set; }
|
|
||||||
public uint ExitCode { get; internal set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static CommandResult RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, IDictionary environment)
|
|
||||||
{
|
|
||||||
UInt32 startup_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
|
|
||||||
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);
|
|
||||||
|
|
||||||
// If lpCurrentDirectory is set to null in PS it will be an empty
|
|
||||||
// string here, we need to convert it
|
|
||||||
if (lpCurrentDirectory == "")
|
|
||||||
lpCurrentDirectory = null;
|
|
||||||
|
|
||||||
StringBuilder environmentString = null;
|
|
||||||
|
|
||||||
if (environment != null && environment.Count > 0)
|
|
||||||
{
|
|
||||||
environmentString = new StringBuilder();
|
|
||||||
foreach (DictionaryEntry kv in environment)
|
|
||||||
environmentString.AppendFormat("{0}={1}\0", kv.Key, kv.Value);
|
|
||||||
environmentString.Append('\0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the environment block if set
|
|
||||||
IntPtr lpEnvironment = IntPtr.Zero;
|
|
||||||
if (environmentString != null)
|
|
||||||
lpEnvironment = Marshal.StringToHGlobalUni(environmentString.ToString());
|
|
||||||
|
|
||||||
// Create console if needed to be inherited by child process
|
|
||||||
bool isConsole = false;
|
|
||||||
if (GetConsoleWindow() == IntPtr.Zero) {
|
|
||||||
isConsole = AllocConsole();
|
|
||||||
|
|
||||||
// Set console input/output codepage to UTF-8
|
|
||||||
SetConsoleCP(65001);
|
|
||||||
SetConsoleOutputCP(65001);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new process and run
|
|
||||||
StringBuilder argument_string = new StringBuilder(lpCommandLine);
|
|
||||||
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
|
|
||||||
if (!CreateProcess(
|
|
||||||
lpApplicationName,
|
|
||||||
argument_string,
|
|
||||||
IntPtr.Zero,
|
|
||||||
IntPtr.Zero,
|
|
||||||
true,
|
|
||||||
startup_flags,
|
|
||||||
lpEnvironment,
|
|
||||||
lpCurrentDirectory,
|
|
||||||
si,
|
|
||||||
out pi))
|
|
||||||
{
|
|
||||||
throw new Win32Exception("Failed to create new process");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy console if we created it
|
|
||||||
if (isConsole) {
|
|
||||||
FreeConsole();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
uint rc = GetProcessExitCode(pi.hProcess);
|
|
||||||
|
|
||||||
return new CommandResult
|
|
||||||
{
|
|
||||||
StandardOut = stdout_str,
|
|
||||||
StandardError = stderr_str,
|
|
||||||
ExitCode = rc
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"@
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
Function Load-CommandUtils {
|
Function Load-CommandUtils {
|
||||||
# makes the following static functions available
|
<#
|
||||||
# [Ansible.CommandUtil]::ParseCommandLine(string lpCommandLine)
|
.SYNOPSIS
|
||||||
# [Ansible.CommandUtil]::SearchPath(string lpFileName)
|
No-op, as the C# types are automatically loaded.
|
||||||
# [Ansible.CommandUtil]::RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, string environmentBlock)
|
#>
|
||||||
#
|
Param()
|
||||||
# there are also numerous P/Invoke methods that can be called if you are feeling adventurous
|
$msg = "Load-CommandUtils is deprecated and no longer needed, this cmdlet will be removed in a future version"
|
||||||
|
if ((Get-Command -Name Add-DeprecationWarning -ErrorAction SilentlyContinue) -and (Get-Variable -Name result -ErrorAction SilentlyContinue)) {
|
||||||
# FUTURE: find a better way to get the _ansible_remote_tmp variable
|
Add-DeprecationWarning -obj $result.Value -message $msg -version 2.12
|
||||||
$original_tmp = $env:TMP
|
} else {
|
||||||
|
$module = Get-Variable -Name module -ErrorAction SilentlyContinue
|
||||||
$remote_tmp = $original_tmp
|
if ($null -ne $module -and $module.Value.GetType().FullName -eq "Ansible.Basic.AnsibleModule") {
|
||||||
$module_params = Get-Variable -Name complex_args -ErrorAction SilentlyContinue
|
$module.Value.Deprecate($msg, "2.12")
|
||||||
if ($module_params) {
|
|
||||||
if ($module_params.Value.ContainsKey("_ansible_remote_tmp") ) {
|
|
||||||
$remote_tmp = $module_params.Value["_ansible_remote_tmp"]
|
|
||||||
$remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$env:TMP = $remote_tmp
|
|
||||||
Add-Type -TypeDefinition $process_util
|
|
||||||
$env:TMP = $original_tmp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Function Get-ExecutablePath($executable, $directory) {
|
Function Get-ExecutablePath {
|
||||||
# lpApplicationName requires the full path to a file, we need to find it
|
<#
|
||||||
# ourselves.
|
.SYNOPSIS
|
||||||
|
Get's the full path to an executable, will search the directory specified or ones in the PATH env var.
|
||||||
|
|
||||||
|
.PARAMETER executable
|
||||||
|
[String]The executable to seach for.
|
||||||
|
|
||||||
|
.PARAMETER directory
|
||||||
|
[String] If set, the directory to search in.
|
||||||
|
|
||||||
|
.OUTPUT
|
||||||
|
[String] The full path the executable specified.
|
||||||
|
#>
|
||||||
|
Param(
|
||||||
|
[String]$executable,
|
||||||
|
[String]$directory = $null
|
||||||
|
)
|
||||||
|
|
||||||
# we need to add .exe if it doesn't have an extension already
|
# we need to add .exe if it doesn't have an extension already
|
||||||
if (-not [System.IO.Path]::HasExtension($executable)) {
|
if (-not [System.IO.Path]::HasExtension($executable)) {
|
||||||
|
@ -404,21 +54,41 @@ Function Get-ExecutablePath($executable, $directory) {
|
||||||
if ($file -ne $null) {
|
if ($file -ne $null) {
|
||||||
$executable_path = $file.FullName
|
$executable_path = $file.FullName
|
||||||
} else {
|
} else {
|
||||||
$executable_path = [Ansible.CommandUtil]::SearchPath($executable)
|
$executable_path = [Ansible.Process.ProcessUtil]::SearchPath($executable)
|
||||||
}
|
}
|
||||||
return $executable_path
|
return $executable_path
|
||||||
}
|
}
|
||||||
|
|
||||||
Function Run-Command {
|
Function Run-Command {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Run a command with the CreateProcess API and return the stdout/stderr and return code.
|
||||||
|
|
||||||
|
.PARAMETER command
|
||||||
|
The full command, including the executable, to run.
|
||||||
|
|
||||||
|
.PARAMETER working_directory
|
||||||
|
The working directory to set on the new process, will default to the current working dir.
|
||||||
|
|
||||||
|
.PARAMETER stdin
|
||||||
|
A string to sent over the stdin pipe to the new process.
|
||||||
|
|
||||||
|
.PARAMETER environment
|
||||||
|
A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars.
|
||||||
|
|
||||||
|
.OUTPUT
|
||||||
|
[Hashtable]
|
||||||
|
[String]executable - The full path to the executable that was run
|
||||||
|
[String]stdout - The stdout stream of the process
|
||||||
|
[String]stderr - The stderr stream of the process
|
||||||
|
[Int32]rc - The return code of the process
|
||||||
|
#>
|
||||||
Param(
|
Param(
|
||||||
[string]$command, # the full command to run including the executable
|
[string]$command,
|
||||||
[string]$working_directory = $null, # the working directory to run under, will default to the current dir
|
[string]$working_directory = $null,
|
||||||
[string]$stdin = $null, # a string to send to the stdin pipe when executing the command
|
[string]$stdin = "",
|
||||||
[hashtable]$environment = @{} # a hashtable of environment values to run the command under, this will replace all the other environment variables with these
|
[hashtable]$environment = @{}
|
||||||
)
|
)
|
||||||
|
|
||||||
# load the C# code we call in this function
|
|
||||||
Load-CommandUtils
|
|
||||||
|
|
||||||
# need to validate the working directory if it is set
|
# need to validate the working directory if it is set
|
||||||
if ($working_directory) {
|
if ($working_directory) {
|
||||||
|
@ -430,11 +100,11 @@ Function Run-Command {
|
||||||
|
|
||||||
# lpApplicationName needs to be the full path to an executable, we do this
|
# lpApplicationName needs to be the full path to an executable, we do this
|
||||||
# by getting the executable as the first arg and then getting the full path
|
# by getting the executable as the first arg and then getting the full path
|
||||||
$arguments = [Ansible.CommandUtil]::ParseCommandLine($command)
|
$arguments = [Ansible.Process.ProcessUtil]::ParseCommandLine($command)
|
||||||
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
|
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
|
||||||
|
|
||||||
# run the command and get the results
|
# run the command and get the results
|
||||||
$command_result = [Ansible.CommandUtil]::RunCommand($executable, $command, $working_directory, $stdin, $environment)
|
$command_result = [Ansible.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin)
|
||||||
|
|
||||||
return ,@{
|
return ,@{
|
||||||
executable = $executable
|
executable = $executable
|
||||||
|
@ -445,4 +115,4 @@ Function Run-Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
# this line must stay at the bottom to ensure all defined module parts are exported
|
# this line must stay at the bottom to ensure all defined module parts are exported
|
||||||
Export-ModuleMember -Alias * -Function * -Cmdlet *
|
Export-ModuleMember -Function Get-ExecutablePath, Load-CommandUtils, Run-Command
|
||||||
|
|
|
@ -143,57 +143,6 @@
|
||||||
- '"LogonUser failed" not in become_invalid_pass.msg'
|
- '"LogonUser failed" not in become_invalid_pass.msg'
|
||||||
- '"Win32ErrorCode 1326)" not in become_invalid_pass.msg'
|
- '"Win32ErrorCode 1326)" not in become_invalid_pass.msg'
|
||||||
|
|
||||||
- name: test become with SYSTEM account
|
|
||||||
win_whoami:
|
|
||||||
become: yes
|
|
||||||
become_method: runas
|
|
||||||
become_user: SYSTEM
|
|
||||||
register: whoami_out
|
|
||||||
|
|
||||||
- name: verify output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- whoami_out.account.sid == "S-1-5-18"
|
|
||||||
- whoami_out.account.account_name == "SYSTEM"
|
|
||||||
- whoami_out.account.domain_name == "NT AUTHORITY"
|
|
||||||
- whoami_out.label.account_name == 'System Mandatory Level'
|
|
||||||
- whoami_out.label.sid == 'S-1-16-16384'
|
|
||||||
- whoami_out.logon_type == 'System'
|
|
||||||
|
|
||||||
- name: test become with NetworkService account
|
|
||||||
win_whoami:
|
|
||||||
become: yes
|
|
||||||
become_method: runas
|
|
||||||
become_user: NetworkService
|
|
||||||
register: whoami_out
|
|
||||||
|
|
||||||
- name: verify output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- whoami_out.account.sid == "S-1-5-20"
|
|
||||||
- whoami_out.account.account_name == "NETWORK SERVICE"
|
|
||||||
- whoami_out.account.domain_name == "NT AUTHORITY"
|
|
||||||
- whoami_out.label.account_name == 'System Mandatory Level'
|
|
||||||
- whoami_out.label.sid == 'S-1-16-16384'
|
|
||||||
- whoami_out.logon_type == 'Service'
|
|
||||||
|
|
||||||
- name: test become with LocalService account
|
|
||||||
win_whoami:
|
|
||||||
become: yes
|
|
||||||
become_method: runas
|
|
||||||
become_user: LocalService
|
|
||||||
register: whoami_out
|
|
||||||
|
|
||||||
- name: verify output
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
- whoami_out.account.sid == "S-1-5-19"
|
|
||||||
- whoami_out.account.account_name == "LOCAL SERVICE"
|
|
||||||
- whoami_out.account.domain_name == "NT AUTHORITY"
|
|
||||||
- whoami_out.label.account_name == 'System Mandatory Level'
|
|
||||||
- whoami_out.label.sid == 'S-1-16-16384'
|
|
||||||
- whoami_out.logon_type == 'Service'
|
|
||||||
|
|
||||||
- name: test become + async
|
- name: test become + async
|
||||||
vars: *become_vars
|
vars: *become_vars
|
||||||
win_command: whoami
|
win_command: whoami
|
||||||
|
@ -228,82 +177,6 @@
|
||||||
register: failed_flags_invalid_flag
|
register: failed_flags_invalid_flag
|
||||||
failed_when: "failed_flags_invalid_flag.msg != \"internal error: 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 != \"internal error: 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
|
|
||||||
- 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
|
|
||||||
vars: *become_vars
|
|
||||||
win_whoami:
|
|
||||||
become_flags: logon_type={{item.type}}
|
|
||||||
register: become_logon_type
|
|
||||||
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
|
|
||||||
with_items:
|
|
||||||
- type: interactive
|
|
||||||
actual: Interactive
|
|
||||||
- type: batch
|
|
||||||
actual: Batch
|
|
||||||
- type: network
|
|
||||||
actual: Network
|
|
||||||
- type: network_cleartext
|
|
||||||
actual: NetworkCleartext
|
|
||||||
|
|
||||||
- name: become netcredentials with network user
|
|
||||||
vars:
|
|
||||||
ansible_become_user: fakeuser
|
|
||||||
ansible_become_password: fakepassword
|
|
||||||
ansible_become_method: runas
|
|
||||||
ansible_become: True
|
|
||||||
ansible_become_flags: logon_type=new_credentials logon_flags=netcredentials_only
|
|
||||||
win_whoami:
|
|
||||||
register: become_netcredentials
|
|
||||||
|
|
||||||
- name: assert become netcredentials with network user
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
# new_credentials still come up as the ansible_user so we can't test that
|
|
||||||
- become_netcredentials.label.account_name == 'High Mandatory Level'
|
|
||||||
- become_netcredentials.label.sid == 'S-1-16-12288'
|
|
||||||
|
|
||||||
- name: become logon_flags bitwise tests when loading the profile
|
|
||||||
# Error code of 2 means no file found == no profile loaded
|
|
||||||
win_shell: |
|
|
||||||
Add-Type -Name "Native" -Namespace "Ansible" -MemberDefinition '[DllImport("Userenv.dll", SetLastError=true)]public static extern bool GetProfileType(out UInt32 pdwFlags);'
|
|
||||||
$profile_type = $null
|
|
||||||
$res = [Ansible.Native]::GetProfileType([ref]$profile_type)
|
|
||||||
if (-not $res) {
|
|
||||||
$last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
||||||
if ($last_err -eq 2) {
|
|
||||||
return $false
|
|
||||||
} else {
|
|
||||||
throw [System.ComponentModel.Win32Exception]$last_err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
vars: *admin_become_vars
|
|
||||||
become_flags: logon_flags={{item.flags}}
|
|
||||||
register: become_logon_flags
|
|
||||||
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:
|
|
||||||
- flags:
|
|
||||||
actual: False
|
|
||||||
- flags: netcredentials_only
|
|
||||||
actual: False
|
|
||||||
- flags: with_profile,netcredentials_only
|
|
||||||
actual: True
|
|
||||||
|
|
||||||
- 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
|
||||||
vars: *become_vars
|
vars: *become_vars
|
||||||
|
@ -348,7 +221,7 @@
|
||||||
win_user:
|
win_user:
|
||||||
name: "{{ become_test_username }}"
|
name: "{{ become_test_username }}"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
- name: ensure privileged test user is deleted
|
- name: ensure privileged test user is deleted
|
||||||
win_user:
|
win_user:
|
||||||
name: "{{ become_test_admin_username }}"
|
name: "{{ become_test_admin_username }}"
|
||||||
|
@ -360,7 +233,7 @@
|
||||||
args:
|
args:
|
||||||
executable: cmd.exe
|
executable: cmd.exe
|
||||||
when: become_test_username in profile_dir_out.stdout_lines[0]
|
when: become_test_username in profile_dir_out.stdout_lines[0]
|
||||||
|
|
||||||
- name: ensure privileged test user profile is deleted
|
- name: ensure privileged test user profile is deleted
|
||||||
# NB: have to work around powershell limitation of long filenames until win_file fixes it
|
# NB: have to work around powershell limitation of long filenames until win_file fixes it
|
||||||
win_shell: rmdir /S /Q {{ admin_profile_dir_out.stdout_lines[0] }}
|
win_shell: rmdir /S /Q {{ admin_profile_dir_out.stdout_lines[0] }}
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
- cmdout is not changed
|
- cmdout is not changed
|
||||||
- cmdout.cmd == 'bogus_command1234'
|
- cmdout.cmd == 'bogus_command1234'
|
||||||
- cmdout.rc == 2
|
- cmdout.rc == 2
|
||||||
- "'Could not locate the following executable bogus_command1234' in cmdout.msg"
|
- "\"Could not find file 'bogus_command1234.exe'.\" in cmdout.msg"
|
||||||
|
|
||||||
- name: execute something with error output
|
- name: execute something with error output
|
||||||
win_command: cmd /c "echo some output & echo some error 1>&2"
|
win_command: cmd /c "echo some output & echo some error 1>&2"
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,229 @@
|
||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Process
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
|
||||||
|
|
||||||
|
Function Assert-Equals {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual,
|
||||||
|
[Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected
|
||||||
|
)
|
||||||
|
|
||||||
|
$matched = $false
|
||||||
|
if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
|
||||||
|
$Actual.Count | Assert-Equals -Expected $Expected.Count
|
||||||
|
for ($i = 0; $i -lt $Actual.Count; $i++) {
|
||||||
|
$actual_value = $Actual[$i]
|
||||||
|
$expected_value = $Expected[$i]
|
||||||
|
Assert-Equals -Actual $actual_value -Expected $expected_value
|
||||||
|
}
|
||||||
|
$matched = $true
|
||||||
|
} else {
|
||||||
|
$matched = $Actual -ceq $Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $matched) {
|
||||||
|
if ($Actual -is [PSObject]) {
|
||||||
|
$Actual = $Actual.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
$call_stack = (Get-PSCallStack)[1]
|
||||||
|
$module.Result.test = $test
|
||||||
|
$module.Result.actual = $Actual
|
||||||
|
$module.Result.expected = $Expected
|
||||||
|
$module.Result.line = $call_stack.ScriptLineNumber
|
||||||
|
$module.Result.method = $call_stack.Position.Text
|
||||||
|
$module.FailJson("AssertionError: actual != expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tests = @{
|
||||||
|
"ParseCommandLine empty string" = {
|
||||||
|
$expected = @((Get-Process -Id $pid).Path)
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("")
|
||||||
|
Assert-Equals -Actual $actual -Expected $expected
|
||||||
|
}
|
||||||
|
|
||||||
|
"ParseCommandLine single argument" = {
|
||||||
|
$expected = @("powershell.exe")
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe")
|
||||||
|
Assert-Equals -Actual $actual -Expected $expected
|
||||||
|
}
|
||||||
|
|
||||||
|
"ParseCommandLine multiple arguments" = {
|
||||||
|
$expected = @("powershell.exe", "-File", "C:\temp\script.ps1")
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe -File C:\temp\script.ps1")
|
||||||
|
Assert-Equals -Actual $actual -Expected $expected
|
||||||
|
}
|
||||||
|
|
||||||
|
"ParseCommandLine comples arguments" = {
|
||||||
|
$expected = @('abc', 'd', 'ef gh', 'i\j', 'k"l', 'm\n op', 'ADDLOCAL=qr, s', 'tuv\', 'w''x', 'yz')
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine('abc d "ef gh" i\j k\"l m\\"n op" ADDLOCAL="qr, s" tuv\ w''x yz')
|
||||||
|
Assert-Equals -Actual $actual -Expected $expected
|
||||||
|
}
|
||||||
|
|
||||||
|
"SearchPath normal" = {
|
||||||
|
$expected = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::SearchPath("powershell.exe")
|
||||||
|
$actual | Assert-Equals -Expected $expected
|
||||||
|
}
|
||||||
|
|
||||||
|
"SearchPath missing" = {
|
||||||
|
$failed = $false
|
||||||
|
try {
|
||||||
|
[Ansible.Process.ProcessUtil]::SearchPath("fake.exe")
|
||||||
|
} catch {
|
||||||
|
$failed = $true
|
||||||
|
$_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "System.IO.FileNotFoundException"
|
||||||
|
$expected = 'Exception calling "SearchPath" with "1" argument(s): "Could not find file ''fake.exe''."'
|
||||||
|
$_.Exception.Message | Assert-Equals -Expected $expected
|
||||||
|
}
|
||||||
|
$failed | Assert-Equals -Expected $true
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess basic" = {
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("whoami.exe")
|
||||||
|
$actual.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Result"
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected "$(&whoami.exe)`r`n"
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess stderr" = {
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe [System.Console]::Error.WriteLine('hi')")
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected ""
|
||||||
|
$actual.StandardError | Assert-Equals -Expected "hi`r`n"
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess exit code" = {
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe exit 10")
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected ""
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 10
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess bad executable" = {
|
||||||
|
$failed = $false
|
||||||
|
try {
|
||||||
|
[Ansible.Process.ProcessUtil]::CreateProcess("fake.exe")
|
||||||
|
} catch {
|
||||||
|
$failed = $true
|
||||||
|
$_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Win32Exception"
|
||||||
|
$expected = 'Exception calling "CreateProcess" with "1" argument(s): "CreateProcessW() failed '
|
||||||
|
$expected += '(The system cannot find the file specified, Win32ErrorCode 2)"'
|
||||||
|
$_.Exception.Message | Assert-Equals -Expected $expected
|
||||||
|
}
|
||||||
|
$failed | Assert-Equals -Expected $true
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with unicode" = {
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("cmd.exe /c echo 💩 café")
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected "💩 café`r`n"
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo 💩 café", $null, $null)
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected "💩 café`r`n"
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess without working dir" = {
|
||||||
|
$expected = $pwd.Path + "`r`n"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', $null, $null)
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with working dir" = {
|
||||||
|
$expected = "C:\Windows`r`n"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', "C:\Windows", $null)
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess without environment" = {
|
||||||
|
$expected = "$($env:USERNAME)`r`n"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $env:TEST; $env:USERNAME', $null, $null)
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with environment" = {
|
||||||
|
$env_vars = @{
|
||||||
|
TEST = "tesTing"
|
||||||
|
TEST2 = "Testing 2"
|
||||||
|
}
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'cmd.exe /c set', $null, $env_vars)
|
||||||
|
("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true
|
||||||
|
("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true
|
||||||
|
("USERNAME=$($env:USERNAME)" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with string stdin" = {
|
||||||
|
$expected = "input value`r`n`r`n"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
|
||||||
|
$null, $null, "input value")
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with string stdin and newline" = {
|
||||||
|
$expected = "input value`r`n`r`n"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
|
||||||
|
$null, $null, "input value`r`n")
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with byte stdin" = {
|
||||||
|
$expected = "input value`r`n"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
|
||||||
|
$null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value"))
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with byte stdin and newline" = {
|
||||||
|
$expected = "input value`r`n`r`n"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
|
||||||
|
$null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value`r`n"))
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
|
||||||
|
"CreateProcess with lpApplicationName" = {
|
||||||
|
$expected = "abc`r`n"
|
||||||
|
$full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "Write-Output 'abc'", $null, $null)
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
|
||||||
|
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "powershell.exe Write-Output 'abc'", $null, $null)
|
||||||
|
$actual.StandardOut | Assert-Equals -Expected $expected
|
||||||
|
$actual.StandardError | Assert-Equals -Expected ""
|
||||||
|
$actual.ExitCode | Assert-Equals -Expected 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($test_impl in $tests.GetEnumerator()) {
|
||||||
|
$test = $test_impl.Key
|
||||||
|
&$test_impl.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
$module.Result.data = "success"
|
||||||
|
$module.ExitJson()
|
||||||
|
|
|
@ -7,3 +7,40 @@
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- ansible_basic_test.data == "success"
|
- ansible_basic_test.data == "success"
|
||||||
|
|
||||||
|
# Users by default don't have this right, temporarily enable it
|
||||||
|
- name: ensure the Users group have the SeBatchLogonRight
|
||||||
|
win_user_right:
|
||||||
|
name: SeBatchLogonRight
|
||||||
|
users:
|
||||||
|
- Users
|
||||||
|
action: add
|
||||||
|
register: batch_user_add
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: test Ansible.Become.cs
|
||||||
|
ansible_become_tests:
|
||||||
|
register: ansible_become_tests
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: remove SeBatchLogonRight from users if added in test
|
||||||
|
win_user_right:
|
||||||
|
name: SeBatchLogonRight
|
||||||
|
users:
|
||||||
|
- Users
|
||||||
|
action: remove
|
||||||
|
when: batch_user_add is changed
|
||||||
|
|
||||||
|
- name: assert test Ansible.Become.cs
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- ansible_become_tests.data == "success"
|
||||||
|
|
||||||
|
- name: test Ansible.Process.cs
|
||||||
|
ansible_process_tests:
|
||||||
|
register: ansible_process_tests
|
||||||
|
|
||||||
|
- name: assert test Ansible.Process.cs
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- ansible_process_tests.data == "success"
|
||||||
|
|
|
@ -34,7 +34,7 @@ try {
|
||||||
$actual = Run-Command -command "C:\fakepath\$exe_filename arg1"
|
$actual = Run-Command -command "C:\fakepath\$exe_filename arg1"
|
||||||
Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
|
Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
|
||||||
} catch {
|
} catch {
|
||||||
Assert-Equals -actual $_.Exception.Message -expected "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not locate the following executable C:\fakepath\$exe_filename`""
|
Assert-Equals -actual $_.Exception.Message -expected "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not find file 'C:\fakepath\$exe_filename'.`""
|
||||||
}
|
}
|
||||||
|
|
||||||
$test_name = "exe in current folder"
|
$test_name = "exe in current folder"
|
||||||
|
|
|
@ -2,6 +2,7 @@ examples/scripts/ConfigureRemotingForAnsible.ps1 PSAvoidUsingCmdletAliases
|
||||||
examples/scripts/upgrade_to_ps3.ps1 PSAvoidUsingWriteHost
|
examples/scripts/upgrade_to_ps3.ps1 PSAvoidUsingWriteHost
|
||||||
examples/scripts/upgrade_to_ps3.ps1 PSUseApprovedVerbs
|
examples/scripts/upgrade_to_ps3.ps1 PSUseApprovedVerbs
|
||||||
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 PSUseApprovedVerbs
|
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.ArgvParser.psm1 PSUseApprovedVerbs
|
||||||
|
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 PSProvideCommentHelp # need to agree on best format for comment location
|
||||||
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 PSUseApprovedVerbs
|
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 PSUseApprovedVerbs
|
||||||
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 PSProvideCommentHelp
|
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.FileUtil.psm1 PSProvideCommentHelp
|
||||||
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 PSAvoidUsingWMICmdlet
|
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 PSAvoidUsingWMICmdlet
|
||||||
|
|
Loading…
Reference in a new issue