Skip to content

Commit

Permalink
win_command - Added cmd and argv options (#371)
Browse files Browse the repository at this point in the history
  • Loading branch information
jborean93 authored Jul 10, 2022
1 parent c482029 commit 83cd290
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 46 deletions.
4 changes: 4 additions & 0 deletions changelogs/fragments/win_command-cmd-argv.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
minor_changes:
- win_command - Migrated to the newer Ansible.Basic style module to improve module invocation output
- win_command - Added the ``cmd`` module option for specifying the command to run as a module option rather than the free form input
- win_command - Added the ``argv`` module option for specifying the command to run as a list to be escaped rather than the free form input
151 changes: 109 additions & 42 deletions plugins/modules/win_command.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,78 +3,145 @@
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

#Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.CommandUtil
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell ..module_utils.Process
#Requires -Module Ansible.ModuleUtils.FileUtil

# TODO: add check mode support
$spec = @{
options = @{
_raw_params = @{ type = "str" }
cmd = @{ type = 'str' }
argv = @{ type = "list"; elements = "str" }
chdir = @{ type = "path" }
creates = @{ type = "path" }
removes = @{ type = "path" }
stdin = @{ type = "str" }
output_encoding_override = @{ type = "str" }
}
required_one_of = @(
, @('_raw_params', 'argv', 'cmd')
)
mutually_exclusive = @(
, @('_raw_params', 'argv', 'cmd')
)
supports_check_mode = $false
}
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)

$chdir = $module.Params.chdir
$creates = $module.Params.creates
$removes = $module.Params.removes
$stdin = $module.Params.stdin
$output_encoding_override = $module.Params.output_encoding_override

<#
There are 3 ways a command can be specified with win_command:
1. Through _raw_params - the value will be used as is
- win_command: raw params here
2. Through cmd - the value will be used as is
Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'
- win_command:
cmd: cmd to run here
$params = Parse-Args $args -supports_check_mode $false
3. Using argv - the values will be escaped using C argument rules
$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true
$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
- win_command:
argv:
- executable
- argument 1
- argument 2
- repeat as needed
$raw_command_line = $raw_command_line.Trim()
Each of these options are mutually exclusive and at least 1 needs to be specified.
#>
$filePath = $null
$rawCmdLine = if ($module.Params.cmd) {
$module.Params.cmd
}
elseif ($module.Params._raw_params) {
$module.Params._raw_params.Trim()
}
else {
$argv = $module.Params.argv

$result = @{
changed = $true
cmd = $raw_command_line
# First resolve just the executable to an absolute path
$filePath = Resolve-ExecutablePath -FilePath $argv[0] -WorkingDirectory $chdir

# Then combine the executable + remaining arguments and escape them
@(
ConvertTo-EscapedArgument -InputObject $filePath
$argv | Select-Object -Skip 1 | ConvertTo-EscapedArgument
) -join " "
}

$module.Result.cmd = $rawCmdLine
$module.Result.rc = 0

if ($creates -and $(Test-AnsiblePath -Path $creates)) {
Exit-Json @{ msg = "skipped, since $creates exists"; cmd = $raw_command_line; changed = $false; skipped = $true; rc = 0 }
$module.Result.msg = "skipped, since $creates exists"
$module.Result.skipped = $true
$module.ExitJson()
}

if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
Exit-Json @{ msg = "skipped, since $removes does not exist"; cmd = $raw_command_line; changed = $false; skipped = $true; rc = 0 }
$module.Result.msg = "skipped, since $removes does not exist"
$module.Result.skipped = $true
$module.ExitJson()
}

$command_args = @{
command = $raw_command_line
$commandParams = @{
CommandLine = $rawCmdLine
}
if ($filePath) {
$commandParams.FilePath = $filePath
}
if ($chdir) {
$command_args['working_directory'] = $chdir
$commandParams.WorkingDirectory = $chdir
}
if ($stdin) {
$command_args['stdin'] = $stdin
$commandParams.InputObject = $stdin
}
if ($output_encoding_override) {
$command_args['output_encoding_override'] = $output_encoding_override
$commandParams.OutputEncodingOverride = $output_encoding_override
}

$start_datetime = [DateTime]::UtcNow
$startDatetime = [DateTime]::UtcNow
try {
$command_result = Run-Command @command_args
$cmdResult = Start-AnsibleWindowsProcess @commandParams
}
catch {
$result.changed = $false
try {
$result.rc = $_.Exception.NativeErrorCode
}
catch {
$result.rc = 2
$module.Result.rc = 2

# Keep on checking inner exceptions to see if it has the NativeErrorCode to
# report back.
$exp = $_.Exception
while ($exp) {
if ($exp.PSObject.Properties.Name -contains 'NativeErrorCode') {
$module.Result.rc = $exp.NativeErrorCode
break
}
$exp = $exp.InnerException
}
Fail-Json -obj $result -message $_.Exception.Message

$module.FailJson("Failed to run: '$rawCmdLine': $($_.Exception.Message)", $_)
}

$result.stdout = $command_result.stdout
$result.stderr = $command_result.stderr
$result.rc = $command_result.rc
$module.Result.cmd = $cmdResult.Command
$module.Result.changed = $true
$module.Result.stdout = $cmdResult.Stdout
$module.Result.stderr = $cmdResult.Stderr
$module.Result.rc = $cmdResult.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")
$endDatetime = [DateTime]::UtcNow
$module.Result.start = $startDatetime.ToString("yyyy-MM-dd HH:mm:ss.ffffff")
$module.Result.end = $endDatetime.ToString("yyyy-MM-dd HH:mm:ss.ffffff")
$module.Result.delta = $($endDatetime - $startDatetime).ToString("h\:mm\:ss\.ffffff")

If ($result.rc -ne 0) {
Fail-Json -obj $result -message "non-zero return code"
If ($module.Result.rc -ne 0) {
$module.FailJson("non-zero return code")
}

Exit-Json $result
$module.ExitJson()
44 changes: 41 additions & 3 deletions plugins/modules/win_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,27 @@
module if you need these features).
- For non-Windows targets, use the M(ansible.builtin.command) module instead.
options:
free_form:
_raw_params:
description:
- The C(win_command) module takes a free form command to run.
- There is no parameter actually named 'free form'. See the examples!
- This is mutually exclusive with the C(cmd) and C(argv) options.
- There is no parameter actually named '_raw_params'. See the examples!
type: str
required: yes
cmd:
description:
- The command and arguments to run.
- This is mutually exclusive with the C(_raw_params) and C(argv) options.
type: str
version_added: '1.11.0'
argv:
description:
- A list that contains the executable and arguments to run.
- The module will attempt to quote the arguments specified based on the
L(Win32 C command-line argument rules,https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments).
- Not all applications use the same quoting rules so the escaping may not work, for those scenarios use C(cmd) instead.
type: list
elements: str
version_added: '1.11.0'
creates:
description:
- A path or path filter pattern; when the referenced path exists on the target host, the task will be skipped.
Expand Down Expand Up @@ -53,6 +68,7 @@
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.
- Do not try to use the older style free form format and the newer style cmd/argv format. See the examples for how both of these formats are defined.
seealso:
- module: ansible.builtin.command
- module: community.windows.psexec
Expand All @@ -64,6 +80,8 @@
'''

EXAMPLES = r'''
# Older style using the free-form and args format. The command is on the same
# line as the module and 'args' is used to define the options for win_command.
- name: Save the result of 'whoami' in 'whoami_out'
ansible.windows.win_command: whoami
register: whoami_out
Expand All @@ -78,6 +96,26 @@
ansible.windows.win_command: powershell.exe -
args:
stdin: Write-Host test
# Newer style using module options. The command and other arguments are
# defined as module options and are indended like another other module.
- name: Run the 'whoami' executable with the '/all' argument
ansible.windows.win_command:
cmd: whoami.exe /all
- name: Run executable in 'C:\Program Files' with a custom chdir
ansible.windows.win_command:
# When using cmd, the arguments need to be quoted manually
cmd: '"C:\Program Files\My Application\run.exe" "argument 1" -force'
chdir: C:\Windows\TEMP
- name: Run executable using argv and have win_command escape the spaces as needed
ansible.windows.win_command:
# When using argv, each entry is quoted in the module
argv:
- C:\Program Files\My Application\run.exe
- argument 1
- -force
'''

RETURN = r'''
Expand Down
93 changes: 92 additions & 1 deletion tests/integration/targets/win_command/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
- cmdout is not changed
- cmdout.cmd == 'bogus_command1234'
- cmdout.rc == 2
- "\"Could not find file 'bogus_command1234.exe'.\" in cmdout.msg"
- '"The system cannot find the file specified" in cmdout.msg'

- name: execute something with error output
win_command: cmd /c "echo some output & echo some error 1>&2"
Expand Down Expand Up @@ -253,3 +253,94 @@
- nonascii_output.stdout_lines|count == 1
- nonascii_output.stdout_lines[0] == 'über den Fußgängerübergang gehen'
- nonascii_output.stderr == ''

- name: expect failure without defined cmd
win_command:
chdir: '{{ win_printargv_path | win_dirname }}'
register: failure_no_cmd
failed_when:
- failure_no_cmd is not failed
- 'failure_no_cmd.msg != "one of the following is required: _raw_params, argv, cmd"'

- name: expect failure when both cmd and argv are defined
win_command:
cmd: my cmd
argv:
- my cmd
register: failure_both_cmd_and_argv
failed_when:
- failure_both_cmd_and_argv is not failed
- 'failure_both_cmd_and_argv.msg != "parameters are mutually exclusive: _raw_params, argv, cmd"'

- name: call binary with cmd
win_command:
cmd: '"{{ win_printargv_path }}" arg1 "arg 2" C:\path\arg "\"quoted arg\""'
register: cmdout

- set_fact:
cmdout_argv: '{{ cmdout.stdout | trim | from_json }}'

- name: assert call to argv binary with absolute path
assert:
that:
- cmdout is changed
- cmdout.rc == 0
- cmdout_argv.args[0] == 'arg1'
- cmdout_argv.args[1] == 'arg 2'
- cmdout_argv.args[2] == 'C:\\path\\arg'
- cmdout_argv.args[3] == '"quoted arg"'

- name: call binary with single argv entry
win_command:
argv:
- whoami
register: cmdout

- name: assert call binary with single argv entry
assert:
that:
- cmdout is changed
- cmdout.rc == 0
- cmdout.stdout != ""

- name: call binary with argv
win_command:
argv:
- "{{ win_printargv_path }}"
- arg1
- "arg 2"
- C:\path\arg
- "\"quoted arg\""
register: cmdout

- set_fact:
cmdout_argv: '{{ cmdout.stdout | trim | from_json }}'

- name: assert call to argv binary with absolute path
assert:
that:
- cmdout is changed
- cmdout.rc == 0
- cmdout_argv.args[0] == 'arg1'
- cmdout_argv.args[1] == 'arg 2'
- cmdout_argv.args[2] == 'C:\\path\\arg'
- cmdout_argv.args[3] == '"quoted arg"'

- name: call binary with argv with relative path
win_command:
argv:
- '{{ win_printargv_path | win_basename }}'
- testing
chdir: '{{ win_printargv_path | win_dirname }}'
register: cmdout

- set_fact:
cmdout_argv: '{{ cmdout.stdout | trim | from_json }}'

- name: assert call to argv binary with relative path
assert:
that:
- cmdout is changed
- cmdout.rc == 0
- cmdout.cmd == win_printargv_path ~ ' testing'
- cmdout_argv.args[0] == 'testing'

0 comments on commit 83cd290

Please sign in to comment.