From 8296511ed0bcd4fb6f004d489bb5d32e781105cb Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Tue, 24 Jan 2017 14:48:58 +0100 Subject: [PATCH] win_psexec: execute cmds on remote systems as any user (#20141) * win_psexec: execute cmds on remote systems as any user This module uses the versatile psexec tool to run any command remotely as any user (incl. domain users). * Add missing documentation Now that this module is deemed acceptable for inclusion, the documentation is an essential part. * win_psexec: Small cosmetic changes * win_psexec: add more options (priority, elevated, ...) * Fixes after more testing * Renamed 'cmd' to 'psexec_command' + more - Also replaced PSObject() with a hash table - Made $chdir of type "path" - Renamed $args to $extra_args * Various improvements - Switched to using booleans for most parameters - Added type 'bool' to boolean parameters - Added 'interactive' parameter - Added 'wait' parameter - Added an interactive example * Added -type "bool" support to Get-AnsibleParam * Fix deadlock * When using `wait:no` return code is PID of process --- lib/ansible/module_utils/powershell.ps1 | 5 +- lib/ansible/modules/windows/win_psexec.ps1 | 194 +++++++++++++++++++++ lib/ansible/modules/windows/win_psexec.py | 160 +++++++++++++++++ 3 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 lib/ansible/modules/windows/win_psexec.ps1 create mode 100644 lib/ansible/modules/windows/win_psexec.py diff --git a/lib/ansible/module_utils/powershell.ps1 b/lib/ansible/module_utils/powershell.ps1 index c489182d0e6..16cc726acd0 100644 --- a/lib/ansible/module_utils/powershell.ps1 +++ b/lib/ansible/module_utils/powershell.ps1 @@ -157,9 +157,12 @@ Function Get-AnsibleParam($obj, $name, $default = $null, $resultobj, $failifempt } } - # Expand environment variables on path-type (Beware: turns $null into "") If ($value -ne $null -and $type -eq "path") { + # Expand environment variables on path-type (Beware: turns $null into "") $value = Expand-Environment($value) + } ElseIf ($type -eq "bool") { + # Convert boolean types to real Powershell booleans + $value = $value | ConvertTo-Bool } $value diff --git a/lib/ansible/modules/windows/win_psexec.ps1 b/lib/ansible/modules/windows/win_psexec.ps1 new file mode 100644 index 00000000000..4d6206e949e --- /dev/null +++ b/lib/ansible/modules/windows/win_psexec.ps1 @@ -0,0 +1,194 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2017, Dag Wieers (@dagwieers) +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +# See also: https://technet.microsoft.com/en-us/sysinternals/pxexec.aspx + +$params = Parse-Args $args + +$command = Get-AnsibleParam -obj $params -name "command" -failifempty $true +$executable = Get-AnsibleParam -obj $params -name "executable" -default "psexec.exe" +$hostnames = Get-AnsibleParam -obj $params -name "hostnames" +$username = Get-AnsibleParam -obj $params -name "username" +$password = Get-AnsibleParam -obj $params -name "password" +$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path" +$wait = Get-AnsibleParam -obj $params -name "wait" -type "bool" -default $true +$noprofile = Get-AnsibleParam -obj $params -name "noprofile" -type "bool" -default $false +$elevated = Get-AnsibleParam -obj $params -name "elevated" -type "bool" -default $false +$limited = Get-AnsibleParam -obj $params -name "limited" -type "bool" -default $false +$system = Get-AnsibleParam -obj $params -name "system" -type "bool" -default $false +$interactive = Get-AnsibleParam -obj $params -name "interactive" -type "bool" -default $false +$priority = Get-AnsibleParam -obj $params -name "priority" -validateset "background","low","belownormal","abovenormal","high","realtime" +$timeout = Get-AnsibleParam -obj $params -name "timeout" +$extra_opts = Get-AnsibleParam -obj $params -name "extra_opts" -default @() + +$result = @{ + changed = $true +} + +If (-Not (Get-Command $executable -ErrorAction SilentlyContinue)) { + Fail-Json $result "Executable '$executable' was not found." +} + +$util_def = @' +using System; +using System.ComponentModel; +using System.IO; +using System.Threading; + +namespace Ansible.Command { + + public static class NativeUtil { + + public 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; + } + } +} +'@ + +$util_type = Add-Type -TypeDefinition $util_def + +$arguments = "" + +# Supports running on local system if not hostname is specified +If ($hostnames -ne $null) { + $arguments = " \\" + $($hostnames | sort -Unique) -join ',' +} + +# Username is optional +If ($username -ne $null) { + $arguments += " -u `"$username`"" +} + +# Password is optional +If ($password -ne $null) { + $arguments += " -p `"$password`"" +} + +If ($chdir -ne $null) { + $arguments += " -w `"$chdir`"" +} + +If ($wait -eq $false) { + $arguments += " -d" +} + +If ($noprofile -eq $true) { + $arguments += " -e" +} + +If ($elevated -eq $true) { + $arguments += " -h" +} + +If ($system -eq $true) { + $arguments += " -s" +} + +If ($interactive -eq $true) { + $arguments += " -i" +} + +If ($limited -eq $true) { + $arguments += " -l" +} + +If ($priority -ne $null) { + $arguments += " -$priority" +} + +If ($timeout -ne $null) { + $arguments += " -n $timeout" +} + +# Add additional advanced options +ForEach ($opt in $extra_opts) { + $arguments += " $opt" +} + +$arguments += " -accepteula" + +$proc = New-Object System.Diagnostics.Process +$psi = $proc.StartInfo +$psi.FileName = $executable +$psi.Arguments = "$arguments $command" +$psi.RedirectStandardOutput = $true +$psi.RedirectStandardError = $true +$psi.UseShellExecute = $false + +# TODO: psexec has a limit to the argument length of 260 (?) +$result.psexec_command = "$executable$arguments $command" + +$start_datetime = [DateTime]::UtcNow + +Try { + $proc.Start() | Out-Null # will always return $true for non shell-exec cases +} Catch [System.ComponentModel.Win32Exception] { + # fail nicely for "normal" error conditions + # FUTURE: this probably won't work on Nano Server + $excep = $_ + $result.rc = $excep.Exception.NativeErrorCode + Fail-Json $result $excep.Exception.Message +} + +$stdout = $stderr = [string] $null + +[Ansible.Command.NativeUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null + +$result.stdout = $stdout +$result.stderr = $stderr + +$proc.WaitForExit() | Out-Null + +If ($wait -eq $true) { + $result.rc = $proc.ExitCode +} else { + $result.rc = 0 + $result.pid = $proc.ExitCode +} + +$end_datetime = [DateTime]::UtcNow + +$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff") + +Exit-Json $result + diff --git a/lib/ansible/modules/windows/win_psexec.py b/lib/ansible/modules/windows/win_psexec.py new file mode 100644 index 00000000000..6fd2e4a63e6 --- /dev/null +++ b/lib/ansible/modules/windows/win_psexec.py @@ -0,0 +1,160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright 2017, Dag Wieers +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + +DOCUMENTATION = r''' +--- +module: win_psexec +version_added: '2.3' +short_description: Runs commands (remotely) as another (privileged) user +description: +- Run commands (remotely) through the PsExec service +- Run commands as another (domain) user (with elevated privileges) +options: + command: + description: + - The command line to run through PsExec (limited to 260 characters). + required: true + executable: + description: + - The location of the PsExec utility (in case it is not located in your PATH). + default: psexec.exe + hostnames: + description: + - The hostnames to run the command. + - If not provided, the command is run locally. + username: + description: + - The (remote) user to run the command as. + - If not provided, the current user is used. + password: + description: + - The password for the (remote) user to run the command as. + - This is mandatory in order authenticate yourself. + chdir: + description: + - Run the command from this (remote) directory. + noprofile: + description: + - Run the command without loading the account's profile. + default: False + elevated: + description: + - Run the command with elevated privileges. + default: False + interactive: + description: + - Run the program so that it interacts with the desktop on the remote system. + default: False + limited: + description: + - Run the command as limited user (strips the Administrators group and allows only privileges assigned to the Users group). + default: False + system: + description: + - Run the remote command in the System account. + default: False + priority: + description: + - Used to run the command at a different priority. + choices: + - background + - low + - belownormal + - abovenormal + - high + - realtime + timeout: + description: + - The connection timeout in seconds + wait: + description: + - Wait for the application to terminate. + - Only use for non-interactive applications. + default: True +requires: [ psexec ] +author: Dag Wieers (@dagwieers) +''' + +EXAMPLES = r''' +# Test the PsExec connection to the local system (target node) with your user +- win_psexec: + command: whoami.exe + +# Run regedit.exe locally (on target node) as SYSTEM and interactively +- win_psexec: + command: regedit.exe + interactive: yes + system: yes + +# Run the setup.exe installer on multiple servers using the Domain Administrator +- win_psexec: + command: E:\setup.exe /i /IACCEPTEULA + hostnames: + - remote_server1 + - remote_server2 + username: DOMAIN\Administrator + password: some_password + priority: high + +# Run PsExec from custom location C:\Program Files\sysinternals\ +- win_psexec: + command: netsh advfirewall set allprofiles state off + executable: C:\Program Files\sysinternals\psexec.exe + hostnames: [ remote_server ] + password: some_password + priority: low +''' + +RETURN = r''' +cmd: + description: The complete command line used by the module, including PsExec call and additional options. + returned: always + type: string + sample: psexec.exe \\remote_server -u DOMAIN\Administrator -p some_password E:\setup.exe +rc: + description: The return code for the command + returned: always + type: int + sample: 0 +stdout: + description: The standard output from the command + returned: always + type: string + sample: Success. +stderr: + description: The error output from the command + returned: always + type: string + sample: Error 15 running E:\setup.exe +msg: + description: Possible error message on failure + returned: failed + type: string + sample: The 'password' parameter is a required parameter. +changed: + description: Whether or not any changes were made. + returned: always + type: bool + sample: True +'''