diff --git a/lib/ansible/cli/adhoc.py b/lib/ansible/cli/adhoc.py index 95ab640ded2..1b615473e00 100644 --- a/lib/ansible/cli/adhoc.py +++ b/lib/ansible/cli/adhoc.py @@ -85,7 +85,7 @@ class AdHocCLI(CLI): return True def _play_ds(self, pattern, async, poll): - check_raw = self.options.module_name in ('command', 'shell', 'script', 'raw') + check_raw = self.options.module_name in ('command', 'win_command', 'shell', 'win_shell', 'script', 'raw') return dict( name = "Ansible Ad-Hoc", hosts = pattern, diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 0cc4c343dc3..606fb3c194d 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -326,8 +326,8 @@ COLOR_DIFF_LINES = get_config(p, 'colors', 'diff_lines', 'ANSIBLE_COLOR_DIFF_LI DIFF_CONTEXT = get_config(p, 'diff', 'context', 'ANSIBLE_DIFF_CONTEXT', 3, integer=True) # non-configurable things -MODULE_REQUIRE_ARGS = ['command', 'shell', 'raw', 'script'] -MODULE_NO_JSON = ['command', 'shell', 'raw'] +MODULE_REQUIRE_ARGS = ['command', 'win_command', 'shell', 'win_shell', 'raw', 'script'] +MODULE_NO_JSON = ['command', 'win_command', 'shell', 'win_shell', 'raw'] DEFAULT_BECOME_PASS = None DEFAULT_SUDO_PASS = None DEFAULT_REMOTE_PASS = None diff --git a/lib/ansible/parsing/mod_args.py b/lib/ansible/parsing/mod_args.py index dbe85b0bad4..a311156ae9e 100644 --- a/lib/ansible/parsing/mod_args.py +++ b/lib/ansible/parsing/mod_args.py @@ -29,7 +29,9 @@ from ansible.template import Templar # For filtering out modules correctly below RAW_PARAM_MODULES = ([ 'command', + 'win_command', 'shell', + 'win_shell', 'script', 'include', 'include_vars', @@ -161,7 +163,7 @@ class ModuleArgsParser: # only internal variables can start with an underscore, so # we don't allow users to set them directy in arguments - if args and action not in ('command', 'shell', 'script', 'raw'): + if args and action not in ('command', 'win_command', 'shell', 'win_shell', 'script', 'raw'): for arg in args: if arg.startswith('_ansible_'): raise AnsibleError("invalid parameter specified for action '%s': '%s'" % (action, arg)) @@ -191,7 +193,7 @@ class ModuleArgsParser: args = thing elif isinstance(thing, string_types): # form is like: local_action: copy src=a dest=b ... pretty common - check_raw = action in ('command', 'shell', 'script', 'raw') + check_raw = action in ('command', 'win_command', 'shell', 'win_shell', 'script', 'raw') args = parse_kv(thing, check_raw=check_raw) elif thing is None: # this can happen with modules which take no params, like ping: @@ -217,7 +219,7 @@ class ModuleArgsParser: action = None args = None - actions_allowing_raw = ('command', 'shell', 'script', 'raw') + actions_allowing_raw = ('command', 'win_command', 'shell', 'win_shell', 'script', 'raw') if isinstance(thing, dict): # form is like: copy: { src: 'a', dest: 'b' } ... common for structured (aka "complex") args thing = thing.copy() diff --git a/test/integration/roles/test_win_command/tasks/main.yml b/test/integration/roles/test_win_command/tasks/main.yml new file mode 100644 index 00000000000..98f39b3c27f --- /dev/null +++ b/test/integration/roles/test_win_command/tasks/main.yml @@ -0,0 +1,134 @@ +- name: execute a command + win_command: whoami /groups + register: cmdout + +- name: validate result + assert: + that: + - cmdout|success + - cmdout|changed + - cmdout.cmd == 'whoami /groups' + - cmdout.delta is match('^\d:(\d){2}:(\d){2}.(\d){6}$') + - cmdout.end is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - cmdout.rc == 0 + - cmdout.start is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - cmdout.stderr == "" + - cmdout.stdout is search('GROUP INFORMATION') + - '"GROUP INFORMATION" in cmdout.stdout_lines' + - cmdout.warnings == [] + +- name: execute something nonexistent + win_command: bogus_command1234 + register: cmdout + ignore_errors: true + +- name: validate result + assert: + that: + - cmdout|failed + - not cmdout|changed + - cmdout.cmd == 'bogus_command1234' + - cmdout.rc == 2 + - cmdout.msg is search('cannot find the file specified') + +- name: execute something with error output + win_command: cmd /c "echo some output & echo some error 1>&2" + register: cmdout + +- name: validate result + assert: + that: + - cmdout|success + - cmdout|changed + - cmdout.cmd == 'cmd /c "echo some output & echo some error 1>&2"' + - cmdout.delta is match('^\d:(\d){2}:(\d){2}.(\d){6}$') + - cmdout.end is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - cmdout.rc == 0 + - cmdout.start is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - cmdout.stderr is search('some error') + - cmdout.stdout == "some output \r\n" + - cmdout.stdout_lines == ["some output "] + - cmdout.warnings == [] + +- name: ensure test file is absent + win_file: + path: c:\testfile.txt + state: absent + +- name: run with creates, should create + win_command: cmd /c "echo $null >> c:\testfile.txt" + args: + creates: c:\testfile.txt + register: cmdout + +- name: validate result + assert: + that: + - cmdout|success + - cmdout|changed + +- name: run again with creates, should skip + win_command: cmd /c "echo $null >> c:\testfile.txt" + args: + creates: c:\testfile.txt + register: cmdout + +- name: validate result + assert: + that: + - cmdout|skipped + - cmdout.msg is search('exists') + +- name: ensure testfile is still present + win_stat: + path: c:\testfile.txt + register: statout + +- name: validate result + assert: + that: + - statout.stat.exists == true + +- name: run with removes, should remove + win_command: cmd /c "del c:\testfile.txt" + args: + removes: c:\testfile.txt + register: cmdout + +- name: validate result + assert: + that: + - cmdout|success + - cmdout|changed + +- name: run again with removes, should skip + win_command: cmd /c "del c:\testfile.txt" + args: + removes: c:\testfile.txt + register: cmdout + +- name: validate result + assert: + that: + - cmdout|skipped + - cmdout.msg is search('does not exist') + +- name: run something with known nonzero exit code + win_command: cmd /c "exit 254" + register: cmdout + ignore_errors: true + +- name: validate result + assert: + that: + - cmdout|failed + - cmdout.rc == 254 + +- name: write large buffer to stdout + win_command: powershell /c "$ba = New-Object byte[] 16384; (New-Object System.Random 32).NextBytes($ba); [Convert]::ToBase64String($ba) | Write-Output" + register: cmdout + +# TODO: fix small buffer deadlock on large write to stderr before stdout has been consumed +#- name: write large buffer to stderr +# win_shell: $ba = New-Object byte[] 16384; (New-Object System.Random 32).NextBytes($ba); [Convert]::ToBase64String($ba) | Write-Error; Write-Output test +# register: cmdout \ No newline at end of file diff --git a/test/integration/roles/test_win_shell/tasks/main.yml b/test/integration/roles/test_win_shell/tasks/main.yml new file mode 100644 index 00000000000..12e9870b9c6 --- /dev/null +++ b/test/integration/roles/test_win_shell/tasks/main.yml @@ -0,0 +1,175 @@ +- name: execute a powershell cmdlet + win_shell: Write-Output "hello from Ansible" + register: shellout + +- name: validate result + assert: + that: + - shellout|success + - shellout|changed + - shellout.cmd == 'Write-Output "hello from Ansible"' + - shellout.delta is match('^\d:(\d){2}:(\d){2}.(\d){6}$') + - shellout.end is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.rc == 0 + - shellout.start is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.stderr == "" + - shellout.stdout == "hello from Ansible\r\n" + - shellout.stdout_lines == ["hello from Ansible"] + - shellout.warnings == [] + +- name: execute a powershell cmdlet with multi-line output + win_shell: Write-Output "hello from Ansible"; Write-Output "another line"; Write-Output "yet another line" + register: shellout + +- name: validate result + assert: + that: + - shellout|success + - shellout|changed + - shellout.cmd == 'Write-Output "hello from Ansible"; Write-Output "another line"; Write-Output "yet another line"' + - shellout.delta is match('^\d:(\d){2}:(\d){2}.(\d){6}$') + - shellout.end is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.rc == 0 + - shellout.start is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.stderr == "" + - shellout.stdout == "hello from Ansible\r\nanother line\r\nyet another line\r\n" + - shellout.stdout_lines == ["hello from Ansible","another line", "yet another line"] + - shellout.warnings == [] + +- name: execute something nonexistent + win_shell: bogus_command1234 + register: shellout + ignore_errors: true + +- name: validate result + assert: + that: + - shellout|failed + - shellout|changed + - shellout.cmd == 'bogus_command1234' + - shellout.delta is match('^\d:(\d){2}:(\d){2}.(\d){6}$') + - shellout.end is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.rc == 1 + - shellout.start is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.stderr is search('not recognized') + - shellout.stdout == "" + - shellout.stdout_lines == [] + - shellout.warnings == [] + +- name: execute something with error output + win_shell: Write-Error "it broke"; Write-Output "some output" + register: shellout + +- name: validate result + assert: + that: + - shellout|success + - shellout|changed + - shellout.cmd == 'Write-Error "it broke"; Write-Output "some output"' + - shellout.delta is match('^\d:(\d){2}:(\d){2}.(\d){6}$') + - shellout.end is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.rc == 0 + - shellout.start is match('^(\d){4}\-(\d){2}\-(\d){2} (\d){2}:(\d){2}:(\d){2}.(\d){6}$') + - shellout.stderr is search('it broke') + - shellout.stdout == "some output\r\n" + - shellout.stdout_lines == ["some output"] + - shellout.warnings == [] + +- name: ensure test file is absent + win_file: + path: c:\testfile.txt + state: absent + +- name: run with creates, should create + win_shell: echo $null >> c:\testfile.txt + args: + creates: c:\testfile.txt + register: shellout + +- name: validate result + assert: + that: + - shellout|success + - shellout|changed + +- name: run again with creates, should skip + win_shell: echo $null >> c:\testfile.txt + args: + creates: c:\testfile.txt + register: shellout + +- name: validate result + assert: + that: + - shellout|skipped + - shellout.msg is search('exists') + +- name: ensure testfile is still present + win_stat: + path: c:\testfile.txt + register: statout + +- name: validate result + assert: + that: + - statout.stat.exists == true + +- name: run with removes, should remove + win_shell: Remove-Item c:\testfile.txt + args: + removes: c:\testfile.txt + register: shellout + +- name: validate result + assert: + that: + - shellout|success + - shellout|changed + +- name: run again with removes, should skip + win_shell: echo $null >> c:\testfile.txt + args: + removes: c:\testfile.txt + register: shellout + +- name: validate result + assert: + that: + - shellout|skipped + - shellout.msg is search('does not exist') + +- name: run something with known nonzero exit code + win_shell: exit 254 + register: shellout + ignore_errors: true + +- name: validate result + assert: + that: + - shellout|failed + - shellout.rc == 254 + +- name: run something via cmd that will fail in powershell + win_shell: echo line1 & echo.line2 + args: + executable: cmd + register: shellout + +- name: validate result + assert: + that: + - shellout|success + - shellout|changed + - shellout.rc == 0 + - shellout.stdout == "line1 \r\nline2\r\n" + - shellout.stdout_lines == ["line1 ", "line2"] + - shellout.stderr == "" + +- name: write large buffer to stdout + win_shell: $ba = New-Object byte[] 16384; (New-Object System.Random 32).NextBytes($ba); [Convert]::ToBase64String($ba) | Write-Output + register: shellout + +# TODO: fix small buffer deadlock on large write to stderr before stdout has been consumed +#- name: write large buffer to stderr +# win_shell: $ba = New-Object byte[] 16384; (New-Object System.Random 32).NextBytes($ba); [Convert]::ToBase64String($ba) | Write-Error; Write-Output test +# register: shellout \ No newline at end of file diff --git a/test/integration/test_win_group3.yml b/test/integration/test_win_group3.yml index 7ade0cb1229..43afe1c4461 100644 --- a/test/integration/test_win_group3.yml +++ b/test/integration/test_win_group3.yml @@ -5,3 +5,5 @@ - { role: test_win_feature, tags: test_win_feature } - { role: test_win_user, tags: test_win_user } - { role: test_win_async_wrapper, tags: test_win_async_wrapper } + - { role: test_win_shell, tags: test_win_shell } + - { role: test_win_command, tags: test_win_command }