From 6daa34e1c59c1b754eee82134086911e431c22b5 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 15 Sep 2016 08:42:10 -0700 Subject: [PATCH] add win_shell/win_command modules + docs (#4827) --- lib/ansible/modules/windows/win_command.ps1 | 131 ++++++++++++++++++++ lib/ansible/modules/windows/win_command.py | 121 ++++++++++++++++++ lib/ansible/modules/windows/win_shell.ps1 | 101 +++++++++++++++ lib/ansible/modules/windows/win_shell.py | 129 +++++++++++++++++++ 4 files changed, 482 insertions(+) create mode 100644 lib/ansible/modules/windows/win_command.ps1 create mode 100644 lib/ansible/modules/windows/win_command.py create mode 100644 lib/ansible/modules/windows/win_shell.ps1 create mode 100644 lib/ansible/modules/windows/win_shell.py diff --git a/lib/ansible/modules/windows/win_command.ps1 b/lib/ansible/modules/windows/win_command.ps1 new file mode 100644 index 00000000000..5e934abe0f0 --- /dev/null +++ b/lib/ansible/modules/windows/win_command.ps1 @@ -0,0 +1,131 @@ +#!powershell +# 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 . + +# WANT_JSON +# POWERSHELL_COMMON + +# TODO: add check mode support + +Set-StrictMode -Version 2 +$ErrorActionPreference = "Stop" + +$parsed_args = Parse-Args $args $false + +$raw_command_line = $(Get-AnsibleParam $parsed_args "_raw_params" -failifempty $true).Trim() +$chdir = Get-AnsibleParam $parsed_args "chdir" +$creates = Get-AnsibleParam $parsed_args "creates" +$removes = Get-AnsibleParam $parsed_args "removes" + +$result = @{changed=$true; warnings=@(); cmd=$raw_command_line} + +If($creates -and $(Test-Path $creates)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $creates exists"; changed=$false; skipped=$true; rc=0} +} + +If($removes -and -not $(Test-Path $removes)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $removes does not exist"; changed=$false; skipped=$true; rc=0} +} + +$util_def = @' +using System; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ansible.Command +{ + public static class NativeUtil + { + [DllImport("shell32.dll", SetLastError = true)] + static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs); + + public static string[] ParseCommandLine(string cmdline) + { + int numArgs; + IntPtr ret = CommandLineToArgvW(cmdline, out numArgs); + + if (ret == IntPtr.Zero) + throw new Exception(String.Format("Error parsing command line: {0}", new Win32Exception(Marshal.GetLastWin32Error()).Message)); + + 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; + } + } +} +'@ + +$util_type = Add-Type -TypeDefinition $util_def + +# FUTURE: extract this code to separate module_utils as Windows module API version of run_command + +$exec_args = $null + +# Parse the command-line with the Win32 parser to get the application name to run. The Win32 parser +# will deal with quoting/escaping for us... +# FUTURE: no longer necessary once we switch to raw Win32 CreateProcess +$parsed_command_line = [Ansible.Command.NativeUtil]::ParseCommandLine($raw_command_line); +$exec_application = $parsed_command_line[0] +If($parsed_command_line.Length -gt 1) { + # lop the application off, then rejoin the args as a single string + $exec_args = $parsed_command_line[1..$($parsed_command_line.Length-1)] -join " " +} + +$proc = New-Object System.Diagnostics.Process +$psi = $proc.StartInfo +$psi.FileName = $exec_application +$psi.Arguments = $exec_args +$psi.RedirectStandardOutput = $true +$psi.RedirectStandardError = $true +$psi.UseShellExecute = $false + +If ($chdir) { + $psi.WorkingDirectory = $chdir +} + +$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 = $_ + Exit-Json @{failed=$true;changed=$false;cmd=$raw_command_line;rc=$excep.Exception.NativeErrorCode;msg=$excep.Exception.Message} +} + +# TODO: resolve potential deadlock here if stderr fills buffer (~4k) before stdout is closed, +# perhaps some async stream pumping with Process Output/ErrorDataReceived events... + +$result.stdout = $proc.StandardOutput.ReadToEnd() +$result.stderr = $proc.StandardError.ReadToEnd() + +$proc.WaitForExit() | Out-Null + +$result.rc = $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") + +ConvertTo-Json -Depth 99 $result diff --git a/lib/ansible/modules/windows/win_command.py b/lib/ansible/modules/windows/win_command.py new file mode 100644 index 00000000000..2d76a0b8534 --- /dev/null +++ b/lib/ansible/modules/windows/win_command.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Ansible, inc +# +# 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 . +# + +DOCUMENTATION = ''' +--- +module: win_command +short_description: Executes a command on a remote Windows node +version_added: 2.2 +description: + - The M(win_command) module takes the command name followed by a list of space-delimited arguments. + - The given command will be executed on all selected nodes. It will not be + processed through the shell, so variables like C($env:HOME) and operations + like C("<"), C(">"), C("|"), and C(";") will not work (use the M(win_shell) + module if you need these features). +options: + free_form: + description: + - the win_command module takes a free form command to run. There is no parameter actually named 'free form'. + See the examples! + required: true + creates: + description: + - a path or path filter pattern; when the referenced path exists on the target host, the task will be skipped. + removes: + description: + - a path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped. + chdir: + description: + - set the specified path as the current working directory before executing a command +notes: + - If you want to run a command through a shell (say you are using C(<), + C(>), C(|), etc), you actually want the M(win_shell) module instead. The + M(win_command) module is much more secure as it's not affected by the user's + environment. + - " C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not exist, use this." +author: + - Matt Davis +''' + +EXAMPLES = ''' +# Example from Ansible Playbooks. +- win_command: whoami + register: whoami_out + +# Run the command only if the specified file does not exist. +- win_command: wbadmin -backupTarget:c:\\backup\\ creates=c:\\backup\\ + +# You can also use the 'args' form to provide the options. This command +# will change the working directory to c:\\somedir\\ and will only run when +# c:\\backup\\ doesn't exist. +- win_command: wbadmin -backupTarget:c:\\backup\\ creates=c:\\backup\\ + args: + chdir: c:\\somedir\\ + creates: c:\\backup\\ +''' + +RETURN = ''' +msg: + description: changed + returned: always + type: boolean + sample: True +start: + description: The command execution start time + returned: always + type: string + sample: '2016-02-25 09:18:26.429568' +end: + description: The command execution end time + returned: always + type: string + sample: '2016-02-25 09:18:26.755339' +delta: + description: The command execution delta time + returned: always + type: string + sample: '0:00:00.325771' +stdout: + description: The command standard output + returned: always + type: string + sample: 'Clustering node rabbit@slave1 with rabbit@master ...' +stderr: + description: The command standard error + returned: always + type: string + sample: 'ls: cannot access foo: No such file or directory' +cmd: + description: The command executed by the task + returned: always + type: string + sample: 'rabbitmqctl join_cluster rabbit@master' +rc: + description: The command return code (0 means success) + returned: always + type: int + sample: 0 +stdout_lines: + description: The command standard output split in lines + returned: always + type: list of strings + sample: [u'Clustering node rabbit@slave1 with rabbit@master ...'] +''' diff --git a/lib/ansible/modules/windows/win_shell.ps1 b/lib/ansible/modules/windows/win_shell.ps1 new file mode 100644 index 00000000000..750ca121ad4 --- /dev/null +++ b/lib/ansible/modules/windows/win_shell.ps1 @@ -0,0 +1,101 @@ +#!powershell +# 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 . + +# WANT_JSON +# POWERSHELL_COMMON + +# TODO: add check mode support + +Set-StrictMode -Version 2 +$ErrorActionPreference = "Stop" + +$parsed_args = Parse-Args $args $false + +$raw_command_line = $(Get-AnsibleParam $parsed_args "_raw_params" -failifempty $true).Trim() +$chdir = Get-AnsibleParam $parsed_args "chdir" +$executable = Get-AnsibleParam $parsed_args "executable" +$creates = Get-AnsibleParam $parsed_args "creates" +$removes = Get-AnsibleParam $parsed_args "removes" + +$result = @{changed=$true; warnings=@(); cmd=$raw_command_line} + +If($creates -and $(Test-Path $creates)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $creates exists"; changed=$false; skipped=$true; rc=0} +} + +If($removes -and -not $(Test-Path $removes)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $removes does not exist"; changed=$false; skipped=$true; rc=0} +} + +$exec_args = $null + +If(-not $executable -or $executable -eq "powershell") { + $exec_application = "powershell" + + # Base64 encode the command so we don't have to worry about the various levels of escaping + $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line)) + + $exec_args = @("-noninteractive", "-encodedcommand", $encoded_command) +} +Else { + # FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter? + $exec_application = $executable + $exec_args = @("/c", $raw_command_line) +} + +$proc = New-Object System.Diagnostics.Process +$psi = $proc.StartInfo +$psi.FileName = $exec_application +$psi.Arguments = $exec_args +$psi.RedirectStandardOutput = $true +$psi.RedirectStandardError = $true +$psi.UseShellExecute = $false + +If ($chdir) { + $psi.WorkingDirectory = $chdir +} + +$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 = $_ + Exit-Json @{failed=$true;changed=$false;cmd=$raw_command_line;rc=$excep.Exception.NativeErrorCode;msg=$excep.Exception.Message} +} + +# TODO: resolve potential deadlock here if stderr fills buffer (~4k) before stdout is closed, +# perhaps some async stream pumping with Process Output/ErrorDataReceived events... + +$result.stdout = $proc.StandardOutput.ReadToEnd() +$result.stderr = $proc.StandardError.ReadToEnd() + +# TODO: decode CLIXML stderr output (and other streams?) + +$proc.WaitForExit() | Out-Null + +$result.rc = $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") + +ConvertTo-Json -Depth 99 $result diff --git a/lib/ansible/modules/windows/win_shell.py b/lib/ansible/modules/windows/win_shell.py new file mode 100644 index 00000000000..7c4dc68df63 --- /dev/null +++ b/lib/ansible/modules/windows/win_shell.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Ansible, inc +# +# 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 . +# + +DOCUMENTATION = ''' +--- +module: win_shell +short_description: Execute shell commands on target hosts. +version_added: 2.2 +description: + - The M(win_shell) module takes the command name followed by a list of space-delimited arguments. + It is similar to the M(win_command) module, but runs + the command via a shell (defaults to PowerShell) on the target host. +options: + free_form: + description: + - the win_shell module takes a free form command to run. There is no parameter actually named 'free form'. + See the examples! + required: true + creates: + description: + - a path or path filter pattern; when the referenced path exists on the target host, the task will be skipped. + removes: + description: + - a path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped. + chdir: + description: + - set the specified path as the current working directory before executing a command + executable: + description: + - change the shell used to execute the command (eg, C(cmd)). The target shell must accept a C(/c) parameter followed by the raw command line to be executed. +notes: + - If you want to run an executable securely and predictably, it may be + better to use the M(win_command) module instead. Best practices when writing + playbooks will follow the trend of using M(win_command) unless M(win_shell) is + explicitly required. When running ad-hoc commands, use your best judgement. + - WinRM will not return from a command execution until all child processes created have exited. Thus, it is not possible to use win_shell to spawn long-running child or background processes. + Consider creating a Windows service for managing background processes. +author: + - Matt Davis +''' + +EXAMPLES = ''' +# Execute a command in the remote shell; stdout goes to the specified +# file on the remote. +- win_shell: C:\\somescript.ps1 >> c:\\somelog.txt + +# Change the working directory to somedir/ before executing the command. +- win_shell: C:\\somescript.ps1 >> c:\\somelog.txt chdir=c:\\somedir + +# You can also use the 'args' form to provide the options. This command +# will change the working directory to somedir/ and will only run when +# somedir/somelog.txt doesn't exist. +- win_shell: C:\\somescript.ps1 >> c:\\somelog.txt + args: + chdir: c:\\somedir + creates: c:\\somelog.txt + +# Run a command under a non-Powershell interpreter (cmd in this case) +- win_shell: echo %HOMEDIR% + args: + executable: cmd + register: homedir_out +''' + +RETURN = ''' +msg: + description: changed + returned: always + type: boolean + sample: True +start: + description: The command execution start time + returned: always + type: string + sample: '2016-02-25 09:18:26.429568' +end: + description: The command execution end time + returned: always + type: string + sample: '2016-02-25 09:18:26.755339' +delta: + description: The command execution delta time + returned: always + type: string + sample: '0:00:00.325771' +stdout: + description: The command standard output + returned: always + type: string + sample: 'Clustering node rabbit@slave1 with rabbit@master ...' +stderr: + description: The command standard error + returned: always + type: string + sample: 'ls: cannot access foo: No such file or directory' +cmd: + description: The command executed by the task + returned: always + type: string + sample: 'rabbitmqctl join_cluster rabbit@master' +rc: + description: The command return code (0 means success) + returned: always + type: int + sample: 0 +stdout_lines: + description: The command standard output split in lines + returned: always + type: list of strings + sample: [u'Clustering node rabbit@slave1 with rabbit@master ...'] +'''