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:
Jordan Borean 2018-12-13 11:15:25 +10:00 committed by Matt Davis
parent b3ac5b637a
commit 190d1ed7f1
13 changed files with 2586 additions and 1105 deletions

View 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.

View file

@ -523,6 +523,42 @@ Because local service accounts do not have passwords, the
``ansible_become_password`` parameter is not required and is ignored if
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
---------------------------
@ -530,8 +566,7 @@ Accounts without a Password
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
variables like normal but either do not define ``ansible_become_pass`` or set
``ansible_become_pass: ''``.
variables like normal but set ``ansible_become_pass: ''``.
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)>`_

View file

@ -5,6 +5,7 @@ param(
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Payload
)
#Requires -Module Ansible.ModuleUtils.AddType
#AnsibleRequires -CSharpUtil Ansible.Become
$ErrorActionPreference = "Stop"
@ -74,18 +75,24 @@ Function Get-BecomeFlags($flags) {
}
Write-AnsibleLog "INFO - loading C# become code" "become_wrapper"
$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"]))
$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"])
$old_tmp = $env:TMP
$env:TMP = $new_tmp
Add-Type -TypeDefinition $become_def -Debug:$false
$env:TMP = $old_tmp
$become_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Become"]))
$process_def = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Payload.csharp_utils["Ansible.Process"]))
Add-CSharpType -References $become_def, $process_def -TempPath $new_tmp -IncludeDebugInfo
$username = $Payload.become_user
$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 {
$logon_type, $logon_flags = Get-BecomeFlags -flags $Payload.become_flags
} catch {
@ -109,7 +116,7 @@ $bootstrap_wrapper = {
&$exec_wrapper
}
$exec_command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($bootstrap_wrapper.ToString()))
$lp_command_line = New-Object System.Text.StringBuilder @("powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -EncodedCommand $exec_command")
$lp_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?
# 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 {
Write-AnsibleLog "INFO - starting become process '$lp_command_line'" "become_wrapper"
$result = [Ansible.Become.BecomeUtil]::RunAsUser($username, $password, $lp_command_line,
$lp_current_directory, $exec_wrapper, $logon_flags, $logon_type)
$result = [Ansible.Become.BecomeUtil]::CreateProcessAsUser($username, $password, $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"
$stdout = $result.StandardOut
try {

File diff suppressed because it is too large Load diff

View 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;
}
}
}

View file

@ -1,393 +1,43 @@
# Copyright (c) 2017 Ansible Project
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
$process_util = @"
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'
#AnsibleRequires -CSharpUtil Ansible.Process
Function Load-CommandUtils {
# makes the following static functions available
# [Ansible.CommandUtil]::ParseCommandLine(string lpCommandLine)
# [Ansible.CommandUtil]::SearchPath(string lpFileName)
# [Ansible.CommandUtil]::RunCommand(string lpApplicationName, string lpCommandLine, string lpCurrentDirectory, string stdinInput, string environmentBlock)
#
# there are also numerous P/Invoke methods that can be called if you are feeling adventurous
# FUTURE: find a better way to get the _ansible_remote_tmp variable
$original_tmp = $env:TMP
$remote_tmp = $original_tmp
$module_params = Get-Variable -Name complex_args -ErrorAction SilentlyContinue
if ($module_params) {
if ($module_params.Value.ContainsKey("_ansible_remote_tmp") ) {
$remote_tmp = $module_params.Value["_ansible_remote_tmp"]
$remote_tmp = [System.Environment]::ExpandEnvironmentVariables($remote_tmp)
<#
.SYNOPSIS
No-op, as the C# types are automatically loaded.
#>
Param()
$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)) {
Add-DeprecationWarning -obj $result.Value -message $msg -version 2.12
} else {
$module = Get-Variable -Name module -ErrorAction SilentlyContinue
if ($null -ne $module -and $module.Value.GetType().FullName -eq "Ansible.Basic.AnsibleModule") {
$module.Value.Deprecate($msg, "2.12")
}
}
$env:TMP = $remote_tmp
Add-Type -TypeDefinition $process_util
$env:TMP = $original_tmp
}
Function Get-ExecutablePath($executable, $directory) {
# lpApplicationName requires the full path to a file, we need to find it
# ourselves.
Function Get-ExecutablePath {
<#
.SYNOPSIS
Get's the full path to an executable, will search the directory specified or ones in the PATH env var.
.PARAMETER executable
[String]The executable to 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
if (-not [System.IO.Path]::HasExtension($executable)) {
@ -404,21 +54,41 @@ Function Get-ExecutablePath($executable, $directory) {
if ($file -ne $null) {
$executable_path = $file.FullName
} else {
$executable_path = [Ansible.CommandUtil]::SearchPath($executable)
$executable_path = [Ansible.Process.ProcessUtil]::SearchPath($executable)
}
return $executable_path
}
Function Run-Command {
<#
.SYNOPSIS
Run a command with the CreateProcess API and return the stdout/stderr and return code.
.PARAMETER command
The full command, including the executable, to run.
.PARAMETER working_directory
The working directory to set on the new process, will default to the current working dir.
.PARAMETER stdin
A string to sent over the stdin pipe to the new process.
.PARAMETER environment
A hashtable of key/value pairs to run with the command. If set, it will replace all other env vars.
.OUTPUT
[Hashtable]
[String]executable - The full path to the executable that was run
[String]stdout - The stdout stream of the process
[String]stderr - The stderr stream of the process
[Int32]rc - The return code of the process
#>
Param(
[string]$command, # the full command to run including the executable
[string]$working_directory = $null, # the working directory to run under, will default to the current dir
[string]$stdin = $null, # a string to send to the stdin pipe when executing the command
[hashtable]$environment = @{} # a hashtable of environment values to run the command under, this will replace all the other environment variables with these
[string]$command,
[string]$working_directory = $null,
[string]$stdin = "",
[hashtable]$environment = @{}
)
# load the C# code we call in this function
Load-CommandUtils
# need to validate the working directory if it is set
if ($working_directory) {
@ -430,11 +100,11 @@ Function Run-Command {
# lpApplicationName needs to be the full path to an executable, we do this
# by getting the executable as the first arg and then getting the full path
$arguments = [Ansible.CommandUtil]::ParseCommandLine($command)
$arguments = [Ansible.Process.ProcessUtil]::ParseCommandLine($command)
$executable = Get-ExecutablePath -executable $arguments[0] -directory $working_directory
# run the command and get the results
$command_result = [Ansible.CommandUtil]::RunCommand($executable, $command, $working_directory, $stdin, $environment)
$command_result = [Ansible.Process.ProcessUtil]::CreateProcess($executable, $command, $working_directory, $environment, $stdin)
return ,@{
executable = $executable
@ -445,4 +115,4 @@ Function Run-Command {
}
# 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

View file

@ -143,57 +143,6 @@
- '"LogonUser failed" 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
vars: *become_vars
win_command: whoami
@ -228,82 +177,6 @@
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\""
# 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
win_command: cmd.exe /c echo über den Fußgängerübergang gehen
vars: *become_vars
@ -348,7 +221,7 @@
win_user:
name: "{{ become_test_username }}"
state: absent
- name: ensure privileged test user is deleted
win_user:
name: "{{ become_test_admin_username }}"
@ -360,7 +233,7 @@
args:
executable: cmd.exe
when: become_test_username in profile_dir_out.stdout_lines[0]
- name: ensure privileged test user profile is deleted
# 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] }}

View file

@ -28,7 +28,7 @@
- cmdout is not changed
- cmdout.cmd == 'bogus_command1234'
- 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
win_command: cmd /c "echo some output & echo some error 1>&2"

File diff suppressed because it is too large Load diff

View file

@ -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()

View file

@ -7,3 +7,40 @@
assert:
that:
- 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"

View file

@ -34,7 +34,7 @@ try {
$actual = Run-Command -command "C:\fakepath\$exe_filename arg1"
Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
} 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"

View file

@ -2,6 +2,7 @@ examples/scripts/ConfigureRemotingForAnsible.ps1 PSAvoidUsingCmdletAliases
examples/scripts/upgrade_to_ps3.ps1 PSAvoidUsingWriteHost
examples/scripts/upgrade_to_ps3.ps1 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.FileUtil.psm1 PSProvideCommentHelp
lib/ansible/module_utils/powershell/Ansible.ModuleUtils.Legacy.psm1 PSAvoidUsingWMICmdlet