From d81249994eed67589e15a3fafb2704c0108e96eb Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 13 Sep 2018 08:50:13 +1000 Subject: [PATCH] win_script: add support for become and centralise exec wrapper builder (#45582) * win_script: add support for become and centralise exec wrapper builder * satisfying the pep8 gods * do not scan for module dependencies when running as a script --- changelogs/fragments/win_script-become.yaml | 2 + lib/ansible/executor/module_common.py | 114 +++++++++++------- lib/ansible/plugins/action/script.py | 19 +-- lib/ansible/plugins/connection/winrm.py | 16 --- lib/ansible/plugins/shell/powershell.py | 2 +- .../win_script/files/test_script_whoami.ps1 | 2 + .../targets/win_script/tasks/main.yml | 19 ++- 7 files changed, 101 insertions(+), 73 deletions(-) create mode 100644 changelogs/fragments/win_script-become.yaml create mode 100644 test/integration/targets/win_script/files/test_script_whoami.ps1 diff --git a/changelogs/fragments/win_script-become.yaml b/changelogs/fragments/win_script-become.yaml new file mode 100644 index 00000000000..db624b6e89d --- /dev/null +++ b/changelogs/fragments/win_script-become.yaml @@ -0,0 +1,2 @@ +minor_changes: +- win_script - added support for running a script with become diff --git a/lib/ansible/executor/module_common.py b/lib/ansible/executor/module_common.py index 969ca8d046c..ed0a231643b 100644 --- a/lib/ansible/executor/module_common.py +++ b/lib/ansible/executor/module_common.py @@ -688,6 +688,69 @@ def _is_binary(b_module_data): return bool(start.translate(None, textchars)) +def _create_powershell_wrapper(b_module_data, module_args, environment, + async_timeout, become, become_method, + become_user, become_password, become_flags, + scan_dependencies=True): + # creates the manifest/wrapper used in PowerShell modules to enable things + # like become and async - this is also called in action/script.py + exec_manifest = dict( + module_entry=to_text(base64.b64encode(b_module_data)), + powershell_modules=dict(), + module_args=module_args, + actions=['exec'], + environment=environment + ) + + exec_manifest['exec'] = to_text(base64.b64encode(to_bytes(leaf_exec))) + + if async_timeout > 0: + exec_manifest["actions"].insert(0, 'async_watchdog') + exec_manifest["async_watchdog"] = to_text( + base64.b64encode(to_bytes(async_watchdog))) + exec_manifest["actions"].insert(0, 'async_wrapper') + exec_manifest["async_wrapper"] = to_text( + base64.b64encode(to_bytes(async_wrapper))) + exec_manifest["async_jid"] = str(random.randint(0, 999999999999)) + exec_manifest["async_timeout_sec"] = async_timeout + + if become and become_method == 'runas': + exec_manifest["actions"].insert(0, 'become') + exec_manifest["become_user"] = become_user + exec_manifest["become_password"] = become_password + exec_manifest['become_flags'] = become_flags + exec_manifest["become"] = to_text( + base64.b64encode(to_bytes(become_wrapper))) + + finder = PSModuleDepFinder() + + # we don't want to scan for any module_utils or other module related flags + # if scan_dependencies=False - action/script sets to False + if scan_dependencies: + finder.scan_module(b_module_data) + + for name, data in finder.modules.items(): + b64_data = to_text(base64.b64encode(data)) + exec_manifest['powershell_modules'][name] = b64_data + + exec_manifest['min_ps_version'] = finder.ps_version + exec_manifest['min_os_version'] = finder.os_version + if finder.become and 'become' not in exec_manifest['actions']: + exec_manifest['actions'].insert(0, 'become') + exec_manifest['become_user'] = 'SYSTEM' + exec_manifest['become_password'] = None + exec_manifest['become_flags'] = None + exec_manifest['become'] = to_text( + base64.b64encode(to_bytes(become_wrapper))) + + # FUTURE: smuggle this back as a dict instead of serializing here; + # the connection plugin may need to modify it + b_json = to_bytes(json.dumps(exec_manifest)) + b_data = exec_wrapper.replace(b"$json_raw = ''", + b"$json_raw = @'\r\n%s\r\n'@" % b_json) + return b_data + + def _find_module_utils(module_name, b_module_data, module_path, module_args, task_vars, templar, module_compression, async_timeout, become, become_method, become_user, become_password, become_flags, environment): """ @@ -867,53 +930,14 @@ def _find_module_utils(module_name, b_module_data, module_path, module_args, tas # it can fail in the presence of the UTF8 BOM commonly added by # Windows text editors shebang = u'#!powershell' - - exec_manifest = dict( - module_entry=to_text(base64.b64encode(b_module_data)), - powershell_modules=dict(), - module_args=module_args, - actions=['exec'], - environment=environment + # create the common exec wrapper payload and set that as the module_data + # bytes + b_module_data = _create_powershell_wrapper( + b_module_data, module_args, environment, async_timeout, become, + become_method, become_user, become_password, become_flags, + scan_dependencies=True ) - exec_manifest['exec'] = to_text(base64.b64encode(to_bytes(leaf_exec))) - - if async_timeout > 0: - exec_manifest["actions"].insert(0, 'async_watchdog') - exec_manifest["async_watchdog"] = to_text(base64.b64encode(to_bytes(async_watchdog))) - exec_manifest["actions"].insert(0, 'async_wrapper') - exec_manifest["async_wrapper"] = to_text(base64.b64encode(to_bytes(async_wrapper))) - exec_manifest["async_jid"] = str(random.randint(0, 999999999999)) - exec_manifest["async_timeout_sec"] = async_timeout - - if become and become_method == 'runas': - exec_manifest["actions"].insert(0, 'become') - exec_manifest["become_user"] = become_user - exec_manifest["become_password"] = become_password - exec_manifest['become_flags'] = become_flags - exec_manifest["become"] = to_text(base64.b64encode(to_bytes(become_wrapper))) - - finder = PSModuleDepFinder() - finder.scan_module(b_module_data) - - for name, data in finder.modules.items(): - b64_data = to_text(base64.b64encode(data)) - exec_manifest['powershell_modules'][name] = b64_data - - exec_manifest['min_ps_version'] = finder.ps_version - exec_manifest['min_os_version'] = finder.os_version - if finder.become and 'become' not in exec_manifest['actions']: - exec_manifest['actions'].insert(0, 'become') - exec_manifest['become_user'] = 'SYSTEM' - exec_manifest['become_password'] = None - exec_manifest['become_flags'] = None - exec_manifest['become'] = to_text(base64.b64encode(to_bytes(become_wrapper))) - - # FUTURE: smuggle this back as a dict instead of serializing here; the connection plugin may need to modify it - module_json = json.dumps(exec_manifest) - - b_module_data = exec_wrapper.replace(b"$json_raw = ''", b"$json_raw = @'\r\n%s\r\n'@" % to_bytes(module_json)) - elif module_substyle == 'jsonargs': module_args_json = to_bytes(json.dumps(module_args)) diff --git a/lib/ansible/plugins/action/script.py b/lib/ansible/plugins/action/script.py index 76e0d5c9f7e..dc8655f9587 100644 --- a/lib/ansible/plugins/action/script.py +++ b/lib/ansible/plugins/action/script.py @@ -22,9 +22,9 @@ import re import shlex from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail, AnsibleActionSkip +from ansible.executor.module_common import _create_powershell_wrapper from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.plugins.action import ActionBase -from ansible.plugins.shell.powershell import exec_wrapper class ActionModule(ActionBase): @@ -124,13 +124,16 @@ class ActionModule(ActionBase): script_cmd = self._connection._shell.wrap_for_exec(script_cmd) exec_data = None - # WinRM requires a special wrapper to work with environment variables - if self._connection.transport == "winrm": - pay = self._connection._create_raw_wrapper_payload(script_cmd, - env_dict) - exec_data = exec_wrapper.replace(b"$json_raw = ''", - b"$json_raw = @'\r\n%s\r\n'@" - % to_bytes(pay)) + # PowerShell runs the script in a special wrapper to enable things + # like become and environment args + if self._connection._shell.SHELL_FAMILY == "powershell": + # FIXME: use a more public method to get the exec payload + pc = self._play_context + exec_data = _create_powershell_wrapper( + to_bytes(script_cmd), {}, env_dict, self._task.async_val, + pc.become, pc.become_method, pc.become_user, + pc.become_pass, pc.become_flags, scan_dependencies=False + ) script_cmd = "-" result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir)) diff --git a/lib/ansible/plugins/connection/winrm.py b/lib/ansible/plugins/connection/winrm.py index 14984372144..e6216e90e6c 100644 --- a/lib/ansible/plugins/connection/winrm.py +++ b/lib/ansible/plugins/connection/winrm.py @@ -119,7 +119,6 @@ from ansible.module_utils.six.moves.urllib.parse import urlunsplit from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils.six import binary_type from ansible.plugins.connection import ConnectionBase -from ansible.plugins.shell.powershell import leaf_exec from ansible.utils.hashing import secure_hash from ansible.utils.path import makedirs_safe @@ -488,21 +487,6 @@ class Connection(ConnectionBase): self.shell_id = None self._connect() - def _create_raw_wrapper_payload(self, cmd, environment=None): - environment = {} if environment is None else environment - - payload = { - 'module_entry': to_text(base64.b64encode(to_bytes(cmd))), - 'powershell_modules': {}, - 'actions': ['exec'], - 'exec': to_text(base64.b64encode(to_bytes(leaf_exec))), - 'environment': environment, - 'min_ps_version': None, - 'min_os_version': None - } - - return json.dumps(payload) - def _wrapper_payload_stream(self, payload, buffer_size=200000): payload_bytes = to_bytes(payload) byte_count = len(payload_bytes) diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index 454784fe472..13da1876b14 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -987,7 +987,7 @@ $exec_wrapper = { $output = $entrypoint.Run($payload) # base64 encode the output so the non-ascii characters are preserved - Write-Output ([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((Write-Output $output)))) + Write-Output ([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((Write-Output ($output | Out-String))))) } # end exec_wrapper Function Dump-Error ($excep, $msg=$null) { diff --git a/test/integration/targets/win_script/files/test_script_whoami.ps1 b/test/integration/targets/win_script/files/test_script_whoami.ps1 new file mode 100644 index 00000000000..79a1c475b71 --- /dev/null +++ b/test/integration/targets/win_script/files/test_script_whoami.ps1 @@ -0,0 +1,2 @@ +whoami.exe +Write-Output "finished" diff --git a/test/integration/targets/win_script/tasks/main.yml b/test/integration/targets/win_script/tasks/main.yml index cee28d3fab3..bba771ba78d 100644 --- a/test/integration/targets/win_script/tasks/main.yml +++ b/test/integration/targets/win_script/tasks/main.yml @@ -210,14 +210,14 @@ # - "test_cmd_result is changed" - name: run test script that takes a boolean parameter - script: test_script_bool.ps1 $true + script: test_script_bool.ps1 $false # use false as that can pick up more errors register: test_script_bool_result - name: check that the script ran and the parameter was treated as a boolean assert: that: - - "test_script_bool_result.stdout_lines[0] == 'System.Boolean'" - - "test_script_bool_result.stdout_lines[1] == 'True'" + - test_script_bool_result.stdout_lines[0] == 'System.Boolean' + - test_script_bool_result.stdout_lines[1] == 'False' - name: run test script that uses envvars script: test_script_with_env.ps1 @@ -272,3 +272,16 @@ that: - test_script_removes_file_check_mode is changed - remove_file_stat.stat.exists + +- name: run test script with become that outputs 2 lines + script: test_script_whoami.ps1 + register: test_script_result_become + become: yes + become_user: SYSTEM + become_method: runas + +- name: check that the script ran and we get both outputs on new lines + assert: + that: + - test_script_result_become.stdout_lines[0]|lower == 'nt authority\\system' + - test_script_result_become.stdout_lines[1] == 'finished'