1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Windows: Use the correct newline sequence for the platform (#21846)

This change to the template action plugin make template use the
platform's native newline_sequence for Jinja.

We also added the option `newline_sequence` to change the newline
sequence using by Jinja if you need to use another newline sequence than
the platform default.

This was previously discussed in
https://github.com/ansible/ansible/issues/16255#issuecomment-278289414

And also relates to issue #21128
This commit is contained in:
Dag Wieers 2017-03-24 03:47:10 +01:00 committed by Matt Davis
parent ef36d7de68
commit ac43a1bbbc
16 changed files with 381 additions and 71 deletions

View file

@ -20,7 +20,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.0',
'supported_by': 'core'} 'supported_by': 'core'}
DOCUMENTATION = ''' DOCUMENTATION = r'''
--- ---
module: template module: template
version_added: historical version_added: historical
@ -51,18 +51,48 @@ options:
description: description:
- Create a backup file including the timestamp information so you can get - Create a backup file including the timestamp information so you can get
the original file back if you somehow clobbered it incorrectly. the original file back if you somehow clobbered it incorrectly.
required: false
choices: [ "yes", "no" ] choices: [ "yes", "no" ]
default: "no" default: "no"
newline_sequence:
description:
- Specify the newline sequence to use for templating files.
choices: [ '\n', '\r', '\r\n' ]
default: '\n'
version_added: '2.3'
block_start_string:
description:
- The string marking the beginning of a block.
default: '{%'
version_added: '2.3'
block_end_string:
description:
- The string marking the end of a block.
default: '%}'
version_added: '2.3'
variable_start_string:
description:
- The string marking the beginning of a print statement.
default: '{{'
version_added: '2.3'
variable_end_string:
description:
- The string marking the end of a print statement.
default: '}}'
version_added: '2.3'
trim_blocks:
description:
- If this is set to True the first newline after a block is removed (block, not variable tag!).
default: "no"
version_added: '2.3'
force: force:
description: description:
- the default is C(yes), which will replace the remote file when contents - 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 are different than the source. If C(no), the file will only be transferred
if the destination does not exist. if the destination does not exist.
required: false
choices: [ "yes", "no" ] choices: [ "yes", "no" ]
default: "yes" default: "yes"
notes: 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 - Including a string that uses a date in the template will result in the template being marked 'changed' each time
- "Since Ansible version 0.9, templates are loaded with C(trim_blocks=True)." - "Since Ansible version 0.9, templates are loaded with C(trim_blocks=True)."
- "Also, you can override jinja2 settings by adding a special header to template file. - "Also, you can override jinja2 settings by adding a special header to template file.
@ -70,8 +100,6 @@ notes:
which changes the variable interpolation markers to [% var %] instead of {{ var }}. 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. 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." raw/endraw in Jinja2 will not work as you expect because templates in Ansible are recursively evaluated."
author: author:
- Ansible Core Team - Ansible Core Team
- Michael DeHaan - Michael DeHaan
@ -80,7 +108,7 @@ extends_documentation_fragment:
- validate - validate
''' '''
EXAMPLES = ''' EXAMPLES = r'''
# Example from Ansible Playbooks # Example from Ansible Playbooks
- template: - template:
src: /mytemplates/foo.j2 src: /mytemplates/foo.j2
@ -97,6 +125,12 @@ EXAMPLES = '''
group: wheel group: wheel
mode: "u=rw,g=r,o=r" mode: "u=rw,g=r,o=r"
# Create a DOS-style text file from a template
- template:
src: config.ini.j2
dest: /share/windows/config.ini
newline_sequence: '\r\n'
# Copy a new "sudoers" file into place, after passing validation with visudo # Copy a new "sudoers" file into place, after passing validation with visudo
- template: - template:
src: /mine/sudoers src: /mine/sudoers

View file

@ -48,16 +48,57 @@ options:
description: description:
- Location to render the template to on the remote machine. - Location to render the template to on the remote machine.
required: true required: true
newline_sequence:
description:
- Specify the newline sequence to use for templating files.
choices: [ '\n', '\r', '\r\n' ]
default: '\r\n'
version_added: '2.3'
block_start_string:
description:
- The string marking the beginning of a block.
default: '{%'
version_added: '2.3'
block_end_string:
description:
- The string marking the end of a block.
default: '%}'
version_added: '2.3'
variable_start_string:
description:
- The string marking the beginning of a print statement.
default: '{{'
version_added: '2.3'
variable_end_string:
description:
- The string marking the end of a print statement.
default: '}}'
version_added: '2.3'
trim_blocks:
description:
- If this is set to True the first newline after a block is removed (block, not variable tag!).
default: "no"
version_added: '2.3'
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"
version_added: '2.3'
notes: notes:
- "templates are loaded with C(trim_blocks=True)." - For other platforms you can use M(template) which uses '\n' as C(newline_sequence).
- By default, windows line endings are not created in the generated file. - Templates are loaded with C(trim_blocks=True).
- "In order to ensure windows line endings are in the generated file, add the following header
as the first line of your template: ``#jinja2: newline_sequence:'\\r\\n'`` and ensure each line
of the template ends with \\\\r\\\\n"
- Beware fetching files from windows machines when creating templates - Beware fetching files from windows machines when creating templates
because certain tools, such as Powershell ISE, and regedit's export facility because certain tools, such as Powershell ISE, and regedit's export facility
add a Byte Order Mark as the first character of the file, which can cause tracebacks. add a Byte Order Mark as the first character of the file, which can cause tracebacks.
- Use "od -cx" to examine your templates for Byte Order Marks. - To find Byte Order Marks in files, use C(Format-Hex <file> -Count 16) on Windows, and use C(od -a -t x1 -N 16 <file>) on Linux.
- "Also, you can override jinja2 settings by adding a special header to template file.
i.e. C(#jinja2:variable_start_string:'[%', variable_end_string:'%]', trim_blocks: False)
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."
author: "Jon Hawkesworth (@jhawkesworth)" author: "Jon Hawkesworth (@jhawkesworth)"
''' '''
@ -66,4 +107,10 @@ EXAMPLES = r'''
win_template: win_template:
src: /mytemplates/file.conf.j2 src: /mytemplates/file.conf.j2
dest: C:\temp\file.conf dest: C:\temp\file.conf
- name: Create a Unix-style file from a Jinja2 template
win_template:
src: unix/config.conf.j2
dest: C:\share\unix\config.conf
newline_sequence: '\n'
''' '''

View file

@ -34,6 +34,7 @@ boolean = C.mk_boolean
class ActionModule(ActionBase): class ActionModule(ActionBase):
TRANSFERS_FILES = True TRANSFERS_FILES = True
DEFAULT_NEWLINE_SEQUENCE = "\n"
def get_checksum(self, dest, all_vars, try_directory=False, source=None, tmp=None): def get_checksum(self, dest, all_vars, try_directory=False, source=None, tmp=None):
try: try:
@ -61,6 +62,19 @@ class ActionModule(ActionBase):
dest = self._task.args.get('dest', None) dest = self._task.args.get('dest', None)
force = boolean(self._task.args.get('force', True)) force = boolean(self._task.args.get('force', True))
state = self._task.args.get('state', None) state = self._task.args.get('state', None)
newline_sequence = self._task.args.get('newline_sequence', self.DEFAULT_NEWLINE_SEQUENCE)
variable_start_string = self._task.args.get('variable_start_string', None)
variable_end_string = self._task.args.get('variable_end_string', None)
block_start_string = self._task.args.get('block_start_string', None)
block_end_string = self._task.args.get('block_end_string', None)
trim_blocks = self._task.args.get('trim_blocks', None)
wrong_sequences = ["\\n", "\\r", "\\r\\n"]
allowed_sequences = ["\n", "\r", "\r\n"]
# We need to convert unescaped sequences to proper escaped sequences for Jinja2
if newline_sequence in wrong_sequences:
newline_sequence = allowed_sequences[wrong_sequences.index(newline_sequence)]
if state is not None: if state is not None:
result['failed'] = True result['failed'] = True
@ -68,6 +82,9 @@ class ActionModule(ActionBase):
elif source is None or dest is None: elif source is None or dest is None:
result['failed'] = True result['failed'] = True
result['msg'] = "src and dest are required" result['msg'] = "src and dest are required"
elif newline_sequence not in allowed_sequences:
result['failed'] = True
result['msg'] = "newline_sequence needs to be one of: \n, \r or \r\n"
else: else:
try: try:
source = self._find_needle('templates', source) source = self._find_needle('templates', source)
@ -117,7 +134,6 @@ class ActionModule(ActionBase):
time.localtime(os.path.getmtime(b_source)) time.localtime(os.path.getmtime(b_source))
) )
searchpath = [] searchpath = []
# set jinja2 internal search path for includes # set jinja2 internal search path for includes
if 'ansible_search_path' in task_vars: if 'ansible_search_path' in task_vars:
@ -135,6 +151,17 @@ class ActionModule(ActionBase):
searchpath = newsearchpath searchpath = newsearchpath
self._templar.environment.loader.searchpath = searchpath self._templar.environment.loader.searchpath = searchpath
self._templar.environment.newline_sequence = newline_sequence
if block_start_string is not None:
self._templar.environment.block_start_string = block_start_string
if block_end_string is not None:
self._templar.environment.block_end_string = block_end_string
if variable_start_string is not None:
self._templar.environment.variable_start_string = variable_start_string
if variable_end_string is not None:
self._templar.environment.variable_end_string = variable_end_string
if trim_blocks is not None:
self._templar.environment.trim_blocks = bool(trim_blocks)
old_vars = self._templar._available_variables old_vars = self._templar._available_variables
self._templar.set_available_variables(temp_vars) self._templar.set_available_variables(temp_vars)
@ -158,6 +185,14 @@ class ActionModule(ActionBase):
diff = {} diff = {}
new_module_args = self._task.args.copy() new_module_args = self._task.args.copy()
# remove newline_sequence from standard arguments
new_module_args.pop('newline_sequence', None)
new_module_args.pop('block_start_string', None)
new_module_args.pop('block_end_string', None)
new_module_args.pop('variable_start_string', None)
new_module_args.pop('variable_end_string', None)
new_module_args.pop('trim_blocks', None)
if (remote_checksum == '1') or (force and local_checksum != remote_checksum): if (remote_checksum == '1') or (force and local_checksum != remote_checksum):
result['changed'] = True result['changed'] = True

View file

@ -26,4 +26,4 @@ from ansible.plugins.action.template import ActionModule as TemplateActionModule
# Even though TemplateActionModule inherits from ActionBase, we still need to # Even though TemplateActionModule inherits from ActionBase, we still need to
# directly inherit from ActionBase to appease the plugin loader. # directly inherit from ActionBase to appease the plugin loader.
class ActionModule(TemplateActionModule, ActionBase): class ActionModule(TemplateActionModule, ActionBase):
pass DEFAULT_NEWLINE_SEQUENCE = '\r\n'

View file

@ -223,12 +223,13 @@ class Templar:
self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string)) self.SINGLE_VAR = re.compile(r"^%s\s*(\w*)\s*%s$" % (self.environment.variable_start_string, self.environment.variable_end_string))
self.block_start = self.environment.block_start_string self._clean_regex = re.compile(r'(?:%s|%s|%s|%s)' % (
self.block_end = self.environment.block_end_string self.environment.variable_start_string,
self.variable_start = self.environment.variable_start_string self.environment.block_start_string,
self.variable_end = self.environment.variable_end_string self.environment.block_end_string,
self._clean_regex = re.compile(r'(?:%s|%s|%s|%s)' % (self.variable_start, self.block_start, self.block_end, self.variable_end)) self.environment.variable_end_string
self._no_type_regex = re.compile(r'.*\|\s*(?:%s)\s*(?:%s)?$' % ('|'.join(C.STRING_TYPE_FILTERS), self.variable_end)) ))
self._no_type_regex = re.compile(r'.*\|\s*(?:%s)\s*(?:%s)?$' % ('|'.join(C.STRING_TYPE_FILTERS), self.environment.variable_end_string))
def _get_filters(self): def _get_filters(self):
''' '''
@ -294,17 +295,17 @@ class Templar:
token = mo.group(0) token = mo.group(0)
token_start = mo.start(0) token_start = mo.start(0)
if token[0] == self.variable_start[0]: if token[0] == self.environment.variable_start_string[0]:
if token == self.block_start: if token == self.environment.block_start_string:
block_openings.append(token_start) block_openings.append(token_start)
elif token == self.variable_start: elif token == self.environment.variable_start_string:
print_openings.append(token_start) print_openings.append(token_start)
elif token[1] == self.variable_end[1]: elif token[1] == self.environment.variable_end_string[1]:
prev_idx = None prev_idx = None
if token == self.block_end and block_openings: if token == self.environment.block_end_string and block_openings:
prev_idx = block_openings.pop() prev_idx = block_openings.pop()
elif token == self.variable_end and print_openings: elif token == self.environment.variable_end_string and print_openings:
prev_idx = print_openings.pop() prev_idx = print_openings.pop()
if prev_idx is not None: if prev_idx is not None:
@ -622,7 +623,7 @@ class Templar:
# newline here if preserve_newlines is False. # newline here if preserve_newlines is False.
res_newlines = _count_newlines_from_end(res) res_newlines = _count_newlines_from_end(res)
if data_newlines > res_newlines: if data_newlines > res_newlines:
res += '\n' * (data_newlines - res_newlines) res += self.environment.newline_sequence * (data_newlines - res_newlines)
return res return res
except (UndefinedError, AnsibleUndefinedVariable) as e: except (UndefinedError, AnsibleUndefinedVariable) as e:
if fail_on_undefined: if fail_on_undefined:

View file

@ -0,0 +1,3 @@
BEGIN
templated_var_loaded
END

View file

@ -1 +1,3 @@
BEGIN
templated_var_loaded templated_var_loaded
END

View file

@ -62,7 +62,7 @@
copy: src=foo.txt dest={{output_dir}}/foo.txt copy: src=foo.txt dest={{output_dir}}/foo.txt
- name: compare templated file to known good - name: compare templated file to known good
shell: diff -w {{output_dir}}/foo.templated {{output_dir}}/foo.txt shell: diff -uw {{output_dir}}/foo.templated {{output_dir}}/foo.txt
register: diff_result register: diff_result
- name: verify templated file matches known good - name: verify templated file matches known good
@ -251,3 +251,121 @@
assert: assert:
that: that:
- "template_result|changed" - "template_result|changed"
- name: change var for the template
set_fact:
templated_var: "templated_var_loaded"
# UNIX TEMPLATE
- name: fill in a basic template (Unix)
template:
src: foo2.j2
dest: '{{ output_dir }}/foo.unix.templated'
register: template_result
- name: verify that the file was marked as changed (Unix)
assert:
that:
- 'template_result|changed'
- name: fill in a basic template again (Unix)
template:
src: foo2.j2
dest: '{{ output_dir }}/foo.unix.templated'
register: template_result2
- name: verify that the template was not changed (Unix)
assert:
that:
- 'not template_result2|changed'
# VERIFY UNIX CONTENTS
- name: copy known good into place (Unix)
copy:
src: foo.unix.txt
dest: '{{ output_dir }}/foo.unix.txt'
- name: Dump templated file (Unix)
command: hexdump -C {{ output_dir }}/foo.unix.templated
- name: Dump expected file (Unix)
command: hexdump -C {{ output_dir }}/foo.unix.txt
- name: compare templated file to known good (Unix)
command: diff -u {{ output_dir }}/foo.unix.templated {{ output_dir }}/foo.unix.txt
register: diff_result
- name: verify templated file matches known good (Unix)
assert:
that:
- 'diff_result.stdout == ""'
- "diff_result.rc == 0"
# DOS TEMPLATE
- name: fill in a basic template (DOS)
template:
src: foo2.j2
dest: '{{ output_dir }}/foo.dos.templated'
newline_sequence: '\r\n'
register: template_result
- name: verify that the file was marked as changed (DOS)
assert:
that:
- 'template_result|changed'
- name: fill in a basic template again (DOS)
template:
src: foo2.j2
dest: '{{ output_dir }}/foo.dos.templated'
newline_sequence: '\r\n'
register: template_result2
- name: verify that the template was not changed (DOS)
assert:
that:
- 'not template_result2|changed'
# VERIFY DOS CONTENTS
- name: copy known good into place (DOS)
copy:
src: foo.dos.txt
dest: '{{ output_dir }}/foo.dos.txt'
- name: Dump templated file (DOS)
command: hexdump -C {{ output_dir }}/foo.dos.templated
- name: Dump expected file (DOS)
command: hexdump -C {{ output_dir }}/foo.dos.txt
- name: compare templated file to known good (DOS)
command: diff -u {{ output_dir }}/foo.dos.templated {{ output_dir }}/foo.dos.txt
register: diff_result
- name: verify templated file matches known good (DOS)
assert:
that:
- 'diff_result.stdout == ""'
- "diff_result.rc == 0"
# VERIFY DOS CONTENTS
- name: copy known good into place (Unix)
copy:
src: foo.unix.txt
dest: '{{ output_dir }}/foo.unix.txt'
- name: Dump templated file (Unix)
command: hexdump -C {{ output_dir }}/foo.unix.templated
- name: Dump expected file (Unix)
command: hexdump -C {{ output_dir }}/foo.unix.txt
- name: compare templated file to known good (Unix)
command: diff -u {{ output_dir }}/foo.unix.templated {{ output_dir }}/foo.unix.txt
register: diff_result
- name: verify templated file matches known good (Unix)
assert:
that:
- 'diff_result.stdout == ""'
- "diff_result.rc == 0"

View file

@ -0,0 +1,3 @@
BEGIN
{{ templated_var }}
END

View file

@ -0,0 +1,3 @@
BEGIN
[% templated_var %]
END

View file

@ -0,0 +1,3 @@
BEGIN
templated_var_loaded
END

View file

@ -0,0 +1,3 @@
BEGIN
templated_var_loaded
END

View file

@ -16,58 +16,109 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
- name: fill in a basic template # DOS TEMPLATE
# win_template: src=foo.j2 dest={{win_output_dir}}/foo.templated mode=0644 - name: fill in a basic template (DOS)
win_template: src=foo.j2 dest={{win_output_dir}}/foo.templated
register: template_result
- assert:
that:
- "'changed' in template_result"
# - "'dest' in template_result"
# - "'group' in template_result"
# - "'gid' in template_result"
# - "'checksum' in template_result"
# - "'owner' in template_result"
# - "'size' in template_result"
# - "'src' in template_result"
# - "'state' in template_result"
# - "'uid' in template_result"
- name: verify that the file was marked as changed
assert:
that:
- "template_result.changed == true"
- name: fill in a basic template again
win_template: win_template:
src: foo.j2 src: foo.j2
dest: "{{win_output_dir}}/foo.templated" dest: '{{ win_output_dir }}/foo.dos.templated'
register: template_result
- name: verify that the file was marked as changed (DOS)
assert:
that:
- 'template_result|changed'
- name: fill in a basic template again (DOS)
win_template:
src: foo.j2
dest: '{{ win_output_dir }}/foo.dos.templated'
register: template_result2 register: template_result2
- name: verify that the template was not changed - name: verify that the template was not changed (DOS)
assert: assert:
that: that:
- "not template_result2|changed" - 'not template_result2|changed'
# VERIFY CONTENTS # VERIFY DOS CONTENTS
- name: copy known good into place (DOS)
win_copy:
src: foo.dos.txt
dest: '{{ win_output_dir }}\\foo.dos.txt'
- name: copy known good into place - name: compare templated file to known good (DOS)
win_copy: src=foo.txt dest={{win_output_dir}}\\foo.txt raw: fc.exe {{ win_output_dir }}\\foo.dos.templated {{ win_output_dir }}\\foo.dos.txt
- name: compare templated file to known good
raw: fc.exe {{win_output_dir}}\\foo.templated {{win_output_dir}}\\foo.txt
register: diff_result register: diff_result
- debug: var=diff_result - debug:
var: diff_result
- name: verify templated file matches known good - name: verify templated file matches known good (DOS)
assert: assert:
that: that:
# - 'diff_result.stdout == ""' - '"FC: no differences encountered" in diff_result.stdout'
- 'diff_result.stdout_lines[1] == "FC: no differences encountered"'
- "diff_result.rc == 0" - "diff_result.rc == 0"
# UNIX TEMPLATE
- name: fill in a basic template (Unix)
win_template:
src: foo.j2
dest: '{{ win_output_dir }}/foo.unix.templated'
newline_sequence: '\n'
register: template_result
- name: verify that the file was marked as changed (Unix)
assert:
that:
- 'template_result|changed'
- name: fill in a basic template again (Unix)
win_template:
src: foo.j2
dest: '{{ win_output_dir }}/foo.unix.templated'
newline_sequence: '\n'
register: template_result2
- name: verify that the template was not changed (Unix)
assert:
that:
- 'not template_result2|changed'
# VERIFY UNIX CONTENTS
- name: copy known good into place (Unix)
win_copy:
src: foo.unix.txt
dest: '{{ win_output_dir }}\\foo.unix.txt'
- name: compare templated file to known good (Unix)
raw: fc.exe {{ win_output_dir }}\\foo.unix.templated {{ win_output_dir }}\\foo.unix.txt
register: diff_result
- debug:
var: diff_result
- name: verify templated file matches known good (Unix)
assert:
that:
- '"FC: no differences encountered" in diff_result.stdout'
# VERIFY DOS CONTENTS
- name: copy known good into place (DOS)
win_copy:
src: foo.dos.txt
dest: '{{ win_output_dir }}\\foo.dos.txt'
- name: compare templated file to known good (DOS)
raw: fc.exe {{ win_output_dir }}\\foo.dos.templated {{ win_output_dir }}\\foo.dos.txt
register: diff_result
- debug:
var: diff_result
- name: verify templated file matches known good (DOS)
assert:
that:
- '"FC: no differences encountered" in diff_result.stdout'
# VERIFY MODE # VERIFY MODE
# can't set file mode on windows so commenting this test out # can't set file mode on windows so commenting this test out
#- name: set file mode #- name: set file mode

View file

@ -1 +1,3 @@
BEGIN
{{ templated_var }} {{ templated_var }}
END

View file

@ -0,0 +1,3 @@
BEGIN
[% templated_var %]
END

View file

@ -4,7 +4,9 @@ grep -rIPl '\r' . \
--exclude-dir .git \ --exclude-dir .git \
--exclude-dir .tox \ --exclude-dir .tox \
| grep -v -F \ | grep -v -F \
-e './test/integration/targets/win_regmerge/templates/win_line_ending.j2' -e './test/integration/targets/template/files/foo.dos.txt' \
-e './test/integration/targets/win_regmerge/templates/win_line_ending.j2' \
-e './test/integration/targets/win_template/files/foo.dos.txt' \
if [ $? -ne 1 ]; then if [ $? -ne 1 ]; then
printf 'One or more file(s) listed above have invalid line endings.\n' printf 'One or more file(s) listed above have invalid line endings.\n'