diff --git a/changelogs/fragments/ini_file-preserve-symlink.yml b/changelogs/fragments/ini_file-preserve-symlink.yml new file mode 100644 index 0000000000..9e49e8a11e --- /dev/null +++ b/changelogs/fragments/ini_file-preserve-symlink.yml @@ -0,0 +1,3 @@ +--- +bugfixes: + - "ini_file - add the ``follow`` paramter to follow the symlinks instead of replacing them (https://github.com/ansible-collections/community.general/pull/6546)." diff --git a/plugins/modules/ini_file.py b/plugins/modules/ini_file.py index 9e4c370dc5..be4a03102b 100644 --- a/plugins/modules/ini_file.py +++ b/plugins/modules/ini_file.py @@ -109,6 +109,13 @@ options: - Allow option without value and without '=' symbol. type: bool default: false + follow: + description: + - This flag indicates that filesystem links, if they exist, should be followed. + - I(follow=true) can modify I(src) when combined with parameters such as I(mode). + type: bool + default: false + version_added: 7.1.0 notes: - While it is possible to add an I(option) without specifying a I(value), this makes no sense. - As of Ansible 2.3, the I(dest) option has been changed to I(path) as default, but I(dest) still works as well. @@ -191,7 +198,7 @@ def update_section_line(changed, section_lines, index, changed_lines, newline, m def do_ini(module, filename, section=None, option=None, values=None, state='present', exclusive=True, backup=False, no_extra_spaces=False, - create=True, allow_no_value=False): + create=True, allow_no_value=False, follow=False): if section is not None: section = to_text(section) @@ -210,15 +217,20 @@ def do_ini(module, filename, section=None, option=None, values=None, after_header='%s (content)' % filename, ) - if not os.path.exists(filename): + if follow and os.path.islink(filename): + target_filename = os.path.realpath(filename) + else: + target_filename = filename + + if not os.path.exists(target_filename): if not create: - module.fail_json(rc=257, msg='Destination %s does not exist!' % filename) - destpath = os.path.dirname(filename) + module.fail_json(rc=257, msg='Destination %s does not exist!' % target_filename) + destpath = os.path.dirname(target_filename) if not os.path.exists(destpath) and not module.check_mode: os.makedirs(destpath) ini_lines = [] else: - with io.open(filename, 'r', encoding="utf-8-sig") as ini_file: + with io.open(target_filename, 'r', encoding="utf-8-sig") as ini_file: ini_lines = [to_text(line) for line in ini_file.readlines()] if module._diff: @@ -404,7 +416,7 @@ def do_ini(module, filename, section=None, option=None, values=None, backup_file = None if changed and not module.check_mode: if backup: - backup_file = module.backup_local(filename) + backup_file = module.backup_local(target_filename) encoded_ini_lines = [to_bytes(line) for line in ini_lines] try: @@ -416,10 +428,10 @@ def do_ini(module, filename, section=None, option=None, values=None, module.fail_json(msg="Unable to create temporary file %s", traceback=traceback.format_exc()) try: - module.atomic_move(tmpfile, filename) + module.atomic_move(tmpfile, target_filename) except IOError: module.ansible.fail_json(msg='Unable to move temporary \ - file %s to %s, IOError' % (tmpfile, filename), traceback=traceback.format_exc()) + file %s to %s, IOError' % (tmpfile, target_filename), traceback=traceback.format_exc()) return (changed, backup_file, diff, msg) @@ -438,7 +450,8 @@ def main(): exclusive=dict(type='bool', default=True), no_extra_spaces=dict(type='bool', default=False), allow_no_value=dict(type='bool', default=False), - create=dict(type='bool', default=True) + create=dict(type='bool', default=True), + follow=dict(type='bool', default=False) ), mutually_exclusive=[ ['value', 'values'] @@ -458,6 +471,7 @@ def main(): no_extra_spaces = module.params['no_extra_spaces'] allow_no_value = module.params['allow_no_value'] create = module.params['create'] + follow = module.params['follow'] if state == 'present' and not allow_no_value and value is None and not values: module.fail_json(msg="Parameter 'value(s)' must be defined if state=present and allow_no_value=False.") @@ -467,7 +481,7 @@ def main(): elif values is None: values = [] - (changed, backup_file, diff, msg) = do_ini(module, path, section, option, values, state, exclusive, backup, no_extra_spaces, create, allow_no_value) + (changed, backup_file, diff, msg) = do_ini(module, path, section, option, values, state, exclusive, backup, no_extra_spaces, create, allow_no_value, follow) if not module.check_mode and os.path.exists(path): file_args = module.load_file_common_arguments(module.params) diff --git a/tests/integration/targets/ini_file/tasks/tests/04-symlink.yml b/tests/integration/targets/ini_file/tasks/tests/04-symlink.yml new file mode 100644 index 0000000000..de677bffad --- /dev/null +++ b/tests/integration/targets/ini_file/tasks/tests/04-symlink.yml @@ -0,0 +1,59 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- block: &prepare + - name: Create the final file + ansible.builtin.copy: + content: | + [main] + foo=BAR + dest: my_original_file.ini + - name: Clean up symlink.ini + ansible.builtin.file: + path: symlink.ini + state: absent + - name: Create a symbolic link + ansible.builtin.file: + src: my_original_file.ini + dest: symlink.ini + state: link + +- name: Set the proxy key on the symlink which will be converted as a file + community.general.ini_file: + path: symlink.ini + section: main + option: proxy + value: 'http://proxy.myorg.org:3128' +- name: Set the proxy key on the final file that is still unchanged + community.general.ini_file: + path: my_original_file.ini + section: main + option: proxy + value: 'http://proxy.myorg.org:3128' + register: result +- ansible.builtin.assert: + that: + - result is changed + +# With follow +- block: *prepare +- name: Set the proxy key on the symlink which will be preserved + community.general.ini_file: + path: symlink.ini + section: main + option: proxy + value: 'http://proxy.myorg.org:3128' + follow: true + register: result +- name: Set the proxy key on the target directly that was changed in the previous step + community.general.ini_file: + path: my_original_file.ini + section: main + option: proxy + value: 'http://proxy.myorg.org:3128' + register: result +- ansible.builtin.assert: + that: + - "not (result is changed)"