From 961484e00d6914c92bea4062cb0025f2ee6df2d9 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Thu, 31 May 2018 11:43:00 -0500 Subject: [PATCH] Add src parameter for uri module that can be used in place of body. Supports binary files (#33689) * First pass at a src parameter that can be used in place of body. Supports binary files * Add test for uri src body * Bump version_added to 2.6 * Close the open file handle * Add uri action plugin that handles src/remote_src * Document remote_src * Remove duplicate info about remote_src * Bump version_added to 2.7 --- lib/ansible/modules/net_tools/basics/uri.py | 50 ++++++++++++++++- lib/ansible/plugins/action/uri.py | 59 +++++++++++++++++++++ test/integration/targets/uri/tasks/main.yml | 31 ++++++++++- 3 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 lib/ansible/plugins/action/uri.py diff --git a/lib/ansible/modules/net_tools/basics/uri.py b/lib/ansible/modules/net_tools/basics/uri.py index e7ccdefaf5..4220eb1c59 100644 --- a/lib/ansible/modules/net_tools/basics/uri.py +++ b/lib/ansible/modules/net_tools/basics/uri.py @@ -133,6 +133,17 @@ options: client authentication. If I(client_cert) contains both the certificate and key, this option is not required. version_added: '2.4' + src: + description: + - Path to file to be submitted to the remote server. Cannot be used with I(body). + version_added: '2.7' + remote_src: + description: + - If C(no), the module will search for src on originating/master machine, if C(yes) the + module will use the C(src) path on the remote/target machine. + type: bool + default: 'no' + version_added: '2.7' notes: - The dependency on httplib2 was removed in Ansible 2.1. - The module returns all the HTTP headers in lower-case. @@ -206,6 +217,19 @@ EXAMPLES = r''' password: "{{ jenkins.password }}" force_basic_auth: yes status_code: 201 + +- name: POST from contents of local file + uri: + url: "https://httpbin.org/post" + method: POST + src: file.json + +- name: POST from contents of remote file + uri: + url: "https://httpbin.org/post" + method: POST + src: /path/to/my/file.json + remote_src: true ''' RETURN = r''' @@ -367,6 +391,19 @@ def uri(module, url, dest, body, body_format, method, headers, socket_timeout): redirected = False redir_info = {} r = {} + + src = module.params['src'] + if src: + try: + headers.update({ + 'Content-Length': os.stat(src).st_size + }) + data = open(src, 'rb') + except OSError: + module.fail_json(msg='Unable to open source file %s' % src, exception=traceback.format_exc()) + else: + data = body + if dest is not None: # Stash follow_redirects, in this block we don't want to follow # we'll reset back to the supplied value soon @@ -393,7 +430,7 @@ def uri(module, url, dest, body, body_format, method, headers, socket_timeout): # Reset follow_redirects back to the stashed value module.params['follow_redirects'] = follow_redirects - resp, info = fetch_url(module, url, data=body, headers=headers, + resp, info = fetch_url(module, url, data=data, headers=headers, method=method, timeout=socket_timeout) try: @@ -403,6 +440,13 @@ def uri(module, url, dest, body, body_format, method, headers, socket_timeout): # may have been stored in the info as 'body' content = info.pop('body', '') + if src: + # Try to close the open file handle + try: + data.close() + except Exception: + pass + r['redirected'] = redirected or info['url'] != url r.update(redir_info) r.update(info) @@ -418,6 +462,7 @@ def main(): url_password=dict(type='str', aliases=['password'], no_log=True), body=dict(type='raw'), body_format=dict(type='str', default='raw', choices=['form-urlencoded', 'json', 'raw']), + src=dict(type='path'), method=dict(type='str', default='GET', choices=['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'PATCH', 'TRACE', 'CONNECT', 'REFRESH']), return_content=dict(type='bool', default=False), follow_redirects=dict(type='str', default='safe', choices=['all', 'no', 'none', 'safe', 'urllib2', 'yes']), @@ -432,7 +477,8 @@ def main(): argument_spec=argument_spec, # TODO: Remove check_invalid_arguments in 2.9 check_invalid_arguments=False, - add_file_common_args=True + add_file_common_args=True, + mutually_exclusive=[['body', 'src']], ) url = module.params['url'] diff --git a/lib/ansible/plugins/action/uri.py b/lib/ansible/plugins/action/uri.py new file mode 100644 index 0000000000..0eea66e7a6 --- /dev/null +++ b/lib/ansible/plugins/action/uri.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# (c) 2015, Brian Coca +# (c) 2018, Matt Martz +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail +from ansible.module_utils._text import to_native +from ansible.module_utils.parsing.convert_bool import boolean +from ansible.plugins.action import ActionBase + + +class ActionModule(ActionBase): + + TRANSFERS_FILES = True + + def run(self, tmp=None, task_vars=None): + if task_vars is None: + task_vars = dict() + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp # tmp no longer has any effect + + src = self._task.args.get('src', None) + remote_src = boolean(self._task.args.get('remote_src', 'no'), strict=False) + + try: + if (src and remote_src) or not src: + # everything is remote, so we just execute the module + # without changing any of the module arguments + raise _AnsibleActionDone(result=self._execute_module(task_vars=task_vars)) + + try: + src = self._find_needle('files', src) + except AnsibleError as e: + raise AnsibleActionFail(to_native(e)) + + tmp_src = self._connection._shell.join_path(self._connection._shell.tmpdir, os.path.basename(src)) + self._transfer_file(src, tmp_src) + self._fixup_perms2((self._connection._shell.tmpdir, tmp_src)) + + new_module_args = self._task.args.copy() + new_module_args.update( + dict( + src=tmp_src, + ) + ) + + result.update(self._execute_module('uri', module_args=new_module_args, task_vars=task_vars)) + except AnsibleAction as e: + result.update(e.result) + finally: + self._remove_tmp_path(self._connection._shell.tmpdir) + return result diff --git a/test/integration/targets/uri/tasks/main.yml b/test/integration/targets/uri/tasks/main.yml index 69f62f8702..abb249dca8 100644 --- a/test/integration/targets/uri/tasks/main.yml +++ b/test/integration/targets/uri/tasks/main.yml @@ -456,6 +456,35 @@ environment: NETRC: "{{ output_dir|expanduser }}/netrc" +- name: Test JSON POST with src + uri: + url: "https://{{ httpbin_host}}/post" + src: pass0.json + method: POST + return_content: true + body_format: json + register: result + +- name: Validate POST with src works + assert: + that: + - result.json.json[0] == 'JSON Test Pattern pass1' + +- name: Test JSON POST with src and remote_src=True + uri: + url: "https://{{ httpbin_host}}/post" + src: "{{ role_path }}/files/pass0.json" + remote_src: true + method: POST + return_content: true + body_format: json + register: result + +- name: Validate POST with src and remote_src=True works + assert: + that: + - result.json.json[0] == 'JSON Test Pattern pass1' + - name: Test follow_redirects=none include_tasks: redirect-none.yml @@ -466,4 +495,4 @@ include_tasks: redirect-urllib2.yml - name: Test follow_redirects=all - include_tasks: redirect-all.yml + include_tasks: redirect-all.yml \ No newline at end of file