Repository: ansible/ansible

Issue:
net_put does not work with binary files 

##### SUMMARY
<!--- Explain the problem briefly -->

In current net_put module, we try to parse source file through template engine to resolve any variable present in file. If file is a binary file like boot image, template engine fails to parse.

https://github.com/ansible/ansible/issues/40013#issuecomment-391555761

we need a option in this module to signal we are transferring a binary raw file and in this mode we should just scp file to destination without having to go through template engine.
  
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
<!--- Insert, BELOW THIS COMMENT, the name of the module, plugin, task or feature.
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path-->

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
2.6
```




Discussion:

Resolving patch:
diff --git a/lib/ansible/modules/network/files/net_put.py b/lib/ansible/modules/network/files/net_put.py
--- a/lib/ansible/modules/network/files/net_put.py
+++ b/lib/ansible/modules/network/files/net_put.py
@@ -42,6 +42,15 @@
       - Filename from src and at default directory of user shell on
         network_os.
     required: no
+  mode:
+    description:
+      - Set the file transfer mode. If mode is set to 'template' then src
+        file will go through jinja2 template engine to replace any vars if
+        present in src file. If mode is set to 'binary' then file will be
+        copied as it is to destination device.
+    default: binary
+    choices: ['binary', 'template']
+    version_added: "2.7"
 
 requirements:
     - "scp"
diff --git a/lib/ansible/plugins/action/net_put.py b/lib/ansible/plugins/action/net_put.py
--- a/lib/ansible/plugins/action/net_put.py
+++ b/lib/ansible/plugins/action/net_put.py
@@ -50,18 +50,13 @@ def run(self, tmp=None, task_vars=None):
             result['msg'] = ('please use network_cli connection type for net_put module')
             return result
 
-        src_file_path_name = self._task.args.get('src')
-
-        try:
-            self._handle_template()
-        except ValueError as exc:
-            return dict(failed=True, msg=to_text(exc))
-
         try:
             src = self._task.args.get('src')
         except KeyError as exc:
             return {'failed': True, 'msg': 'missing required argument: %s' % exc}
 
+        src_file_path_name = src
+
         # Get destination file if specified
         dest = self._task.args.get('dest')
 
@@ -70,22 +65,38 @@ def run(self, tmp=None, task_vars=None):
         if proto is None:
             proto = 'scp'
 
-        sock_timeout = play_context.timeout
+        # Get mode if set
+        mode = self._task.args.get('mode')
+        if mode is None:
+            mode = 'binary'
 
-        # Now src has resolved file write to disk in current diectory for scp
-        filename = str(uuid.uuid4())
-        cwd = self._loader.get_basedir()
-        output_file = cwd + '/' + filename
-        with open(output_file, 'w') as f:
-            f.write(src)
+        if mode == 'template':
+            try:
+                self._handle_template()
+            except ValueError as exc:
+                return dict(failed=True, msg=to_text(exc))
+
+            # Now src has resolved file write to disk in current diectory for scp
+            src = self._task.args.get('src')
+            filename = str(uuid.uuid4())
+            cwd = self._loader.get_basedir()
+            output_file = cwd + '/' + filename
+            with open(output_file, 'w') as f:
+                f.write(src)
+        else:
+            try:
+                output_file = self._get_binary_src_file(src)
+            except ValueError as exc:
+                return dict(failed=True, msg=to_text(exc))
 
         if socket_path is None:
             socket_path = self._connection.socket_path
 
         conn = Connection(socket_path)
+        sock_timeout = conn.get_option('persistent_command_timeout')
+
         if dest is None:
             dest = src_file_path_name
-
         try:
             out = conn.copy_file(
                 source=output_file, destination=dest,
@@ -101,8 +112,10 @@ def run(self, tmp=None, task_vars=None):
                 result['failed'] = True
                 result['msg'] = ('Exception received : %s' % exc)
 
-        # Cleanup tmp file expanded wih ansible vars
-        os.remove(output_file)
+        if mode == 'template':
+            # Cleanup tmp file expanded wih ansible vars
+            os.remove(output_file)
+
         result['changed'] = True
         return result
 
@@ -165,3 +178,18 @@ def _get_network_os(self, task_vars):
             raise AnsibleError('ansible_network_os must be specified on this host to use platform agnostic modules')
 
         return network_os
+
+    def _get_binary_src_file(self, src):
+        working_path = self._get_working_path()
+
+        if os.path.isabs(src) or urlsplit('src').scheme:
+            source = src
+        else:
+            source = self._loader.path_dwim_relative(working_path, 'templates', src)
+            if not source:
+                source = self._loader.path_dwim_relative(working_path, src)
+
+        if not os.path.exists(source):
+            raise ValueError('path specified in src not found')
+
+        return source


Test patch:
diff --git a/test/integration/targets/ios_file/tests/cli/net_put.yaml b/test/integration/targets/ios_file/tests/cli/net_put.yaml
--- a/test/integration/targets/ios_file/tests/cli/net_put.yaml
+++ b/test/integration/targets/ios_file/tests/cli/net_put.yaml
@@ -31,4 +31,20 @@
     that:
       - result.changed == true
 
+- name: copy file with non-ascii characters to ios in template mode(Fail case)
+  net_put:
+    src: nonascii.bin
+    mode: 'template'
+  register: result
+  ignore_errors: true
+
+- name: copy file with non-ascii characters to ios in default mode(binary)
+  net_put:
+    src: nonascii.bin
+  register: result
+
+- assert:
+    that:
+      - result.changed == true
+
 - debug: msg="END ios cli/net_put.yaml on connection={{ ansible_connection }}"