diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index 8d1b62af75..72fa010137 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -101,6 +101,44 @@ except ImportError: import syslog has_journal = False +try: + from ast import literal_eval as _literal_eval +except ImportError: + # a replacement for literal_eval that works with python 2.4. from: + # https://mail.python.org/pipermail/python-list/2009-September/551880.html + # which is essentially a cut/past from an earlier (2.6) version of python's + # ast.py + from compiler import parse + from compiler.ast import * + def _literal_eval(node_or_string): + """ + Safely evaluate an expression node or a string containing a Python + expression. The string or node provided may only consist of the following + Python literal structures: strings, numbers, tuples, lists, dicts, booleans, + and None. + """ + _safe_names = {'None': None, 'True': True, 'False': False} + if isinstance(node_or_string, basestring): + node_or_string = parse(node_or_string, mode='eval') + if isinstance(node_or_string, Expression): + node_or_string = node_or_string.node + def _convert(node): + if isinstance(node, Const) and isinstance(node.value, (basestring, int, float, long, complex)): + return node.value + elif isinstance(node, Tuple): + return tuple(map(_convert, node.nodes)) + elif isinstance(node, List): + return list(map(_convert, node.nodes)) + elif isinstance(node, Dict): + return dict((_convert(k), _convert(v)) for k, v in node.items) + elif isinstance(node, Name): + if node.name in _safe_names: + return _safe_names[node.name] + elif isinstance(node, UnarySub): + return -_convert(node.expr) + raise ValueError('malformed string') + return _convert(node_or_string) + FILE_COMMON_ARGUMENTS=dict( src = dict(), mode = dict(), @@ -700,6 +738,38 @@ class AnsibleModule(object): else: self.fail_json(msg="internal error: do not know how to interpret argument_spec") + def safe_eval(self, str, locals=None, include_exceptions=False): + + # do not allow method calls to modules + if not isinstance(str, basestring): + # already templated to a datastructure, perhaps? + if include_exceptions: + return (str, None) + return str + if re.search(r'\w\.\w+\(', str): + if include_exceptions: + return (str, None) + return str + # do not allow imports + if re.search(r'import \w+', str): + if include_exceptions: + return (str, None) + return str + try: + result = None + if not locals: + result = _literal_eval(str) + else: + result = _literal_eval(str, None, locals) + if include_exceptions: + return (result, None) + else: + return result + except Exception, e: + if include_exceptions: + return (str, e) + return str + def _check_argument_types(self): ''' ensure all arguments have the requested type ''' for (k, v) in self.argument_spec.iteritems(): diff --git a/library/files/lineinfile b/library/files/lineinfile index 457435bc98..8521fd0895 100644 --- a/library/files/lineinfile +++ b/library/files/lineinfile @@ -19,6 +19,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +import pipes import re import os import tempfile @@ -359,9 +360,14 @@ def main(): if ins_bef is None and ins_aft is None: ins_aft = 'EOF' - # Replace the newline character with an actual newline. Don't replace - # escaped \\n, hence sub and not str.replace. - line = re.sub(r'\n', os.linesep, params['line']) + # Replace escape sequences like '\n' while being sure + # not to replace octal escape sequences (\ooo) since they + # match the backref syntax + if backrefs: + line = re.sub(r'(\\[0-9]{1,3})', r'\\\1', params['line']) + else: + line = params['line'] + line = module.safe_eval(pipes.quote(line)) present(module, dest, params['regexp'], line, ins_aft, ins_bef, create, backup, backrefs) diff --git a/test/integration/roles/test_lineinfile/tasks/main.yml b/test/integration/roles/test_lineinfile/tasks/main.yml index 4b4dfb98e4..5a6feea900 100644 --- a/test/integration/roles/test_lineinfile/tasks/main.yml +++ b/test/integration/roles/test_lineinfile/tasks/main.yml @@ -114,7 +114,7 @@ - "result.stat.md5 == 'd5955ee042139dfef16dbe3a7334475f'" - name: replace a line with backrefs - lineinfile: dest={{output_dir}}/test.txt state=present line="This is line 3" backrefs=yes regexp="^(REF) .* \1$" + lineinfile: dest={{output_dir}}/test.txt state=present line="This is line 3" backrefs=yes regexp="^(REF) .* \\1$" register: result - name: assert that the line with backrefs was changed @@ -224,6 +224,16 @@ - "result.changed == true" - "result.msg == 'line added'" +- name: insert a multiple lines at the end of the file + lineinfile: dest={{output_dir}}/test.txt state=present line="This is a line\nwith \\\n character" insertafter="EOF" + register: result + +- name: assert that the multiple lines was inserted + assert: + that: + - "result.changed == true" + - "result.msg == 'line added'" + - name: testnoeof stat the no newline EOF test after the insert at the end stat: path={{output_dir}}/testnoeof.txt register: result @@ -256,3 +266,31 @@ assert: that: - "result.stat.md5 == '357dcbee8dfb4436f63bab00a235c45a'" + +- stat: path={{output_dir}}/test.txt + register: result + +- name: assert test md5 matches after insert the multiple lines + assert: + that: + - "result.stat.md5 == 'c2510d5bc8fdef8e752b8f8e74c784c2'" + +- name: replace a line with backrefs included in the line + lineinfile: dest={{output_dir}}/test.txt state=present line="New \\1 created with the backref" backrefs=yes regexp="^This is (line 4)$" + register: result + +- name: assert that the line with backrefs was changed + assert: + that: + - "result.changed == true" + - "result.msg == 'line replaced'" + +- name: stat the test after the backref line was replaced + stat: path={{output_dir}}/test.txt + register: result + +- name: assert test md5 matches after backref line was replaced + assert: + that: + - "result.stat.md5 == '65f955c2a9722fd43d07103d7756ff9b'" +