From bd03dd67ffbb1ec965c6e75a3a3acc2370f18b3e Mon Sep 17 00:00:00 2001 From: Ales Nosek Date: Wed, 28 Oct 2015 22:04:32 -0700 Subject: [PATCH] ini_file should only change what was specified and nothing more #5860 See also: http://alesnosek.com/blog/2015/08/03/improving-ansibles-ini-file-module/ --- lib/ansible/modules/files/ini_file.py | 129 +++++++++++++------------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/lib/ansible/modules/files/ini_file.py b/lib/ansible/modules/files/ini_file.py index cb3edb2cff2..5d6df779cf0 100644 --- a/lib/ansible/modules/files/ini_file.py +++ b/lib/ansible/modules/files/ini_file.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # (c) 2012, Jan-Piet Mens +# (c) 2015, Ales Nosek # # This file is part of Ansible # @@ -28,7 +29,7 @@ description: - Manage (add, remove, change) individual settings in an INI-style file without having to manage the file as a whole with, say, M(template) or M(assemble). Adds missing sections if they don't exist. - - Comments are discarded when the source file is read, and therefore will not + - Comments are discarded when the source file is read, and therefore will not show up in the destination file. version_added: "0.9" options: @@ -79,7 +80,7 @@ notes: Either use M(template) to create a base INI file with a C([default]) section, or use M(lineinfile) to add the missing line. requirements: [ ConfigParser ] -author: "Jan-Piet Mens (@jpmens)" +author: "Jan-Piet Mens (@jpmens), Ales Nosek" ''' EXAMPLES = ''' @@ -101,79 +102,77 @@ import sys def do_ini(module, filename, section=None, option=None, value=None, state='present', backup=False): + + with open(filename, 'r') as ini_file: + ini_lines = ini_file.readlines() + # append a fake section line to simplify the logic + ini_lines.append('[') + + within_section = not section + section_start = 0 changed = False - if (sys.version_info[0] == 2 and sys.version_info[1] >= 7) or sys.version_info[0] >= 3: - cp = ConfigParser.ConfigParser(allow_no_value=True) - else: - cp = ConfigParser.ConfigParser() - cp.optionxform = identity - try: - f = open(filename) - cp.readfp(f) - except IOError: - pass - - - if state == 'absent': - if option is None: - changed = cp.remove_section(section) - else: - try: - changed = cp.remove_option(section, option) - except ConfigParser.NoSectionError: - # Option isn't present if the section isn't either - pass - - if state == 'present': - - # DEFAULT section is always there by DEFAULT, so never try to add it. - if not cp.has_section(section) and section.upper() != 'DEFAULT': - - cp.add_section(section) - changed = True - - if option is not None and value is not None: - try: - oldvalue = cp.get(section, option) - if str(value) != str(oldvalue): - cp.set(section, option, value) + for index, line in enumerate(ini_lines): + if line.startswith('[%s]' % section): + within_section = True + section_start = index + elif line.startswith('['): + if within_section: + if state == 'present': + # insert missing option line at the end of the section + ini_lines.insert(index, '%s = %s\n' % (option, value)) changed = True - except ConfigParser.NoSectionError: - cp.set(section, option, value) - changed = True - except ConfigParser.NoOptionError: - cp.set(section, option, value) - changed = True - except ConfigParser.InterpolationError: - cp.set(section, option, value) - changed = True + elif state == 'absent' and not option: + # remove the entire section + del ini_lines[section_start:index] + changed = True + break + else: + if within_section and option: + if state == 'present': + # change the existing option line + if re.match('%s *=' % option, line) \ + or re.match('# *%s *=' % option, line) \ + or re.match('; *%s *=' % option, line): + newline = '%s = %s\n' % (option, value) + changed = ini_lines[index] != newline + ini_lines[index] = newline + if changed: + # remove all possible option occurences from the rest of the section + index = index + 1 + while index < len(ini_lines): + line = ini_lines[index] + if line.startswith('['): + break + if re.match('%s *=' % option, line): + del ini_lines[index] + else: + index = index + 1 + break + else: + # comment out the existing option line + if re.match('%s *=' % option, line): + ini_lines[index] = '#%s' % ini_lines[index] + changed = True + break + + # remove the fake section line + del ini_lines[-1:] + + if not within_section and option and state == 'present': + ini_lines.append('[%s]\n' % section) + ini_lines.append('%s = %s\n' % (option, value)) + changed = True + if changed and not module.check_mode: if backup: module.backup_local(filename) - - try: - f = open(filename, 'w') - cp.write(f) - except: - module.fail_json(msg="Can't create %s" % filename) + with open(filename, 'w') as ini_file: + ini_file.writelines(ini_lines) return changed -# ============================================================== -# identity - -def identity(arg): - """ - This function simply returns its argument. It serves as a - replacement for ConfigParser.optionxform, which by default - changes arguments to lower case. The identity function is a - better choice than str() or unicode(), because it is - encoding-agnostic. - """ - return arg - # ============================================================== # main