Repository: ansible/ansible

Issue:
Add content option to template module
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Feature Idea

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
template

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
v2.2+

##### SUMMARY
<!--- Explain the problem briefly -->
Being able to template a file inline (in the playbook itself) would be super-useful in those cases the content is very short and self-contained (e.g. a templated list, or templated content).

Also the `content:` option documentation in the **copy** module refers the **template** module if you need templated content.:
```
When used instead of 'src', sets the contents of a file directly to the specified value. This is for
simple values, for anything complex or with formatting please switch to the template module.
```



Discussion:
Why not use the `copy` module, and set the `content` parameter, exactly as you mention:

```yaml
- copy:
     content: "{{ my_variable }}"
```
Well, what I wanted to do was something like:

```yaml
- hosts: all
  tasks:
  - copy:
      content: |
        HOSTNAME;IPADDRESS;FQDN;OSNAME;OSVERSION;PROCESSOR;ARCHITECTURE;MEMORY;
        {% for host in hostvars %}
        {%   set vars = hostvars[host|string] %}
        {{ vars.ansible_hostname }};{{ vars.ansible_host }};{{ vars.ansible_fqdn }};{{ vars.ansible_distribution }};{{ vars.ansible_distribution_version }};{{ vars.ansible_processor[1] }};{{ vars.ansible_architecture }};{{ (vars.ansible_memtotal_mb/1024)|round|int }};
        {% endfor %}
      dest: /some/path/systems.csv
      backup: yes
    run_once: yes
    connection: local
```

But reading the documentation of the **copy** module, it appears that it only works for *simple values*. That is why I made this request.

Since the above appears to work, I will send a PR changing the documentation a bit and adding the above example. Nevertheless, I would assume most people would be looking at the template module, and not the copy module for doing the above.
copy was INTENDED for simple values .. that people use it that way ... makes me want to remove the feature.

Resolving patch:
diff --git a/lib/ansible/modules/files/copy.py b/lib/ansible/modules/files/copy.py
--- a/lib/ansible/modules/files/copy.py
+++ b/lib/ansible/modules/files/copy.py
@@ -22,12 +22,11 @@
                     'status': ['stableinterface'],
                     'supported_by': 'core'}
 
-
-DOCUMENTATION = '''
+DOCUMENTATION = r'''
 ---
 module: copy
 version_added: "historical"
-short_description: Copies files to remote locations.
+short_description: Copies files to remote locations
 description:
     - The C(copy) module copies a file from the local or remote machine to a location on the remote machine.
       Use the M(fetch) module to copy files from remote locations to the local box.
@@ -41,69 +40,58 @@
         with "/", only inside contents of that directory are copied to destination.
         Otherwise, if it does not end with "/", the directory itself with all contents
         is copied. This behavior is similar to Rsync.
-    required: false
-    default: null
-    aliases: []
   content:
-    version_added: "1.1"
     description:
       - When used instead of 'src', sets the contents of a file directly to the specified value.
-        This is for simple values, for anything complex or with formatting please switch to the template module.
-    required: false
-    default: null
+        For anything advanced or with formatting also look at the template module.
+    version_added: "1.1"
   dest:
     description:
-      - Remote absolute path where the file should be copied to. If src is a directory,
+      - Remote absolute path where the file should be copied to. If C(src) is a directory,
         this must be a directory too.
-    required: true
-    default: null
+    required: yes
   backup:
     description:
       - Create a backup file including the timestamp information so you can get
         the original file back if you somehow clobbered it incorrectly.
+    type: bool
+    default: 'no'
     version_added: "0.7"
-    required: false
-    choices: [ "yes", "no" ]
-    default: "no"
   force:
     description:
       - the default is C(yes), which will replace the remote file when contents
         are different than the source. If C(no), the file will only be transferred
         if the destination does not exist.
-    version_added: "1.1"
-    required: false
-    choices: [ "yes", "no" ]
-    default: "yes"
+    type: bool
+    default: 'yes'
     aliases: [ "thirsty" ]
+    version_added: "1.1"
   directory_mode:
     description:
       - When doing a recursive copy set the mode for the directories. If this is not set we will use the system
         defaults. The mode is only set on directories which are newly created, and will not affect those that
         already existed.
-    required: false
     version_added: "1.5"
   remote_src:
     description:
-      - If False, it will search for src at originating/master machine, if True it will go to the remote/target machine for the src. Default is False.
-      - Currently remote_src does not support recursive copying.
-    choices: [ "True", "False" ]
-    required: false
-    default: "False"
+      - If C(no), it will search for src at originating/master machine.
+      - If C(yes), it will go to the remote/target machine for the src. Default is False.
+      - Currently C(remote_src) does not support recursive copying.
+    type: bool
+    default: 'no'
     version_added: "2.0"
   follow:
-    required: false
-    default: "no"
-    choices: [ "yes", "no" ]
-    version_added: "1.8"
     description:
-      - 'This flag indicates that filesystem links in the destination, if they exist, should be followed.'
+      - This flag indicates that filesystem links in the destination, if they exist, should be followed.
+    type: bool
+    default: 'no'
+    version_added: "1.8"
   local_follow:
-    required: false
-    default: "yes"
-    choices: [ "yes", "no" ]
-    version_added: "2.4"
     description:
-      - 'This flag indicates that filesystem links in the source tree, if they exist, should be followed.'
+      - This flag indicates that filesystem links in the source tree, if they exist, should be followed.
+    type: bool
+    default: 'yes'
+    version_added: "2.4"
 extends_documentation_fragment:
     - files
     - validate
@@ -117,7 +105,7 @@
    - For Windows targets, use the M(win_copy) module instead.
 '''
 
-EXAMPLES = '''
+EXAMPLES = r'''
 # Example from Ansible Playbooks
 - copy:
     src: /srv/myfiles/foo.conf
@@ -132,7 +120,7 @@
     dest: /etc/foo.conf
     owner: foo
     group: foo
-    mode: "u=rw,g=r,o=r"
+    mode: u=rw,g=r,o=r
 
 # Another symbolic mode example, adding some permissions and removing others
 - copy:
@@ -140,7 +128,7 @@
     dest: /etc/foo.conf
     owner: foo
     group: foo
-    mode: "u+rw,g-wx,o-rwx"
+    mode: u+rw,g-wx,o-rwx
 
 # Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version
 - copy:
@@ -155,41 +143,57 @@
 - copy:
     src: /mine/sudoers
     dest: /etc/sudoers
-    validate: 'visudo -cf %s'
+    validate: visudo -cf %s
 
 # Copy a "sudoers" file on the remote machine for editing
 - copy:
-    remote_src: true
     src: /etc/sudoers
     dest: /etc/sudoers.edit
+    remote_src: yes
+    validate: visudo -cf %s
+
+# Create a CSV file from your complete inventory using an inline template
+- hosts: all
+  tasks:
+  - copy:
+      content: |
+        HOSTNAME;IPADDRESS;FQDN;OSNAME;OSVERSION;PROCESSOR;ARCHITECTURE;MEMORY;
+        {% for host in hostvars %}
+        {%   set vars = hostvars[host|string] %}
+        {{ vars.ansible_hostname }};{{ vars.remote_host }};{{ vars.ansible_fqdn }};{{ vars.ansible_distribution }};{{ vars.ansible_distribution_version }};{{ vars.ansible_processor[1] }};{{ vars.ansible_architecture }};{{ (vars.ansible_memtotal_mb/1024)|round|int }};  # NOQA
+        {% endfor %}
+      dest: /some/path/systems.csv
+      backup: yes
+    run_once: yes
+    delegate_to: localhost
 '''
 
-RETURN = '''
+RETURN = r'''
 dest:
     description: destination file/path
     returned: success
     type: string
-    sample: "/path/to/file.txt"
+    sample: /path/to/file.txt
 src:
     description: source file used for the copy on the target machine
     returned: changed
     type: string
-    sample: "/home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source"
+    sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
 md5sum:
     description: md5 checksum of the file after running copy
     returned: when supported
     type: string
-    sample: "2a5aeecc61dc98c4d780b14b330e3282"
+    sample: 2a5aeecc61dc98c4d780b14b330e3282
 checksum:
     description: sha1 checksum of the file after running copy
     returned: success
     type: string
-    sample: "6e642bb8dd5c2e027bf21dd923337cbb4214f827"
+    sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
 backup_file:
     description: name of backup file created
     returned: changed and if backup=yes
     type: string
-    sample: "/path/to/file.txt.2015-02-12@22:09~"
+    sample: /path/to/file.txt.2015-02-12@22:09~
 gid:
     description: group id of the file, after execution
     returned: success
@@ -199,12 +203,12 @@
     description: group of the file, after execution
     returned: success
     type: string
-    sample: "httpd"
+    sample: httpd
 owner:
     description: owner of the file, after execution
     returned: success
     type: string
-    sample: "httpd"
+    sample: httpd
 uid:
     description: owner id of the file, after execution
     returned: success
@@ -214,7 +218,7 @@
     description: permissions of the target, after execution
     returned: success
     type: string
-    sample: "0644"
+    sample: 0644
 size:
     description: size of the target, after execution
     returned: success
@@ -224,7 +228,7 @@
     description: state of the target, after execution
     returned: success
     type: string
-    sample: "file"
+    sample: file
 '''
 
 import os
@@ -270,17 +274,17 @@ def main():
 
     module = AnsibleModule(
         # not checking because of daisy chain to file module
-        argument_spec = dict(
-            src               = dict(required=False, type='path'),
-            original_basename = dict(required=False),  # used to handle 'dest is a directory' via template, a slight hack
-            content           = dict(required=False, no_log=True),
-            dest              = dict(required=True, type='path'),
-            backup            = dict(default=False, type='bool'),
-            force             = dict(default=True, aliases=['thirsty'], type='bool'),
-            validate          = dict(required=False, type='str'),
-            directory_mode    = dict(required=False, type='raw'),
-            remote_src        = dict(required=False, type='bool'),
-            local_follow      = dict(required=False, type='bool'),
+        argument_spec=dict(
+            src=dict(type='path'),
+            original_basename=dict(type='str'),  # used to handle 'dest is a directory' via template, a slight hack
+            content=dict(type='str', no_log=True),
+            dest=dict(type='path', required=True),
+            backup=dict(type='bool', default=False),
+            force=dict(type='bool', default=True, aliases=['thirsty']),
+            validate=dict(type='str'),
+            directory_mode=dict(type='raw'),
+            remote_src=dict(type='bool'),
+            local_follow=dict(type='bool'),
         ),
         add_file_common_args=True,
         supports_check_mode=True,
diff --git a/lib/ansible/modules/files/template.py b/lib/ansible/modules/files/template.py
--- a/lib/ansible/modules/files/template.py
+++ b/lib/ansible/modules/files/template.py
@@ -19,12 +19,11 @@
                     'status': ['stableinterface'],
                     'supported_by': 'core'}
 
-
 DOCUMENTATION = r'''
 ---
 module: template
 version_added: historical
-short_description: Templates a file out to a remote server.
+short_description: Templates a file out to a remote server
 description:
      - Templates are processed by the Jinja2 templating language
        (U(http://jinja.pocoo.org/docs/)) - documentation on the template
@@ -51,8 +50,8 @@
     description:
       - Create a backup file including the timestamp information so you can get
         the original file back if you somehow clobbered it incorrectly.
-    choices: [ "yes", "no" ]
-    default: "no"
+    type: bool
+    default: 'no'
   newline_sequence:
     description:
       - Specify the newline sequence to use for templating files.
@@ -82,15 +81,16 @@
   trim_blocks:
     description:
       - If this is set to True the first newline after a block is removed (block, not variable tag!).
-    default: "no"
+    type: bool
+    default: 'no'
     version_added: '2.4'
   force:
     description:
       - the default is C(yes), which will replace the remote file when contents
         are different than the source.  If C(no), the file will only be transferred
         if the destination does not exist.
-    choices: [ "yes", "no" ]
-    default: "yes"
+    type: bool
+    default: 'yes'
 notes:
   - For Windows you can use M(win_template) which uses '\r\n' as C(newline_sequence).
   - Including a string that uses a date in the template will result in the template being marked 'changed' each time
@@ -100,6 +100,8 @@
     which changes the variable interpolation markers to  [% var %] instead of  {{ var }}.
     This is the best way to prevent evaluation of things that look like, but should not be Jinja2.
     raw/endraw in Jinja2 will not work as you expect because templates in Ansible are recursively evaluated."
+  - You can use the C(copy) module with the C(content:) option if you prefer the template inline,
+    as part of the playbook.
 author:
     - Ansible Core Team
     - Michael DeHaan


Test patch:
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -230,7 +230,6 @@ lib/ansible/modules/files/acl.py
 lib/ansible/modules/files/archive.py
 lib/ansible/modules/files/assemble.py
 lib/ansible/modules/files/blockinfile.py
-lib/ansible/modules/files/copy.py
 lib/ansible/modules/files/ini_file.py
 lib/ansible/modules/files/iso_extract.py
 lib/ansible/modules/files/replace.py