mirror of
				https://github.com/ansible-collections/community.general.git
				synced 2024-09-14 20:13:21 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			326 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			326 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/python
 | |
| # -*- coding: utf-8 -*-
 | |
| 
 | |
| # (c) 2012, Daniel Hokka Zakrisson <daniel@hozac.com>
 | |
| #
 | |
| # This file is part of Ansible
 | |
| #
 | |
| # Ansible is free software: you can redistribute it and/or modify
 | |
| # it under the terms of the GNU General Public License as published by
 | |
| # the Free Software Foundation, either version 3 of the License, or
 | |
| # (at your option) any later version.
 | |
| #
 | |
| # Ansible is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU General Public License for more details.
 | |
| #
 | |
| # You should have received a copy of the GNU General Public License
 | |
| # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| import re
 | |
| import os
 | |
| import tempfile
 | |
| 
 | |
| DOCUMENTATION = """
 | |
| ---
 | |
| module: lineinfile
 | |
| author: Daniel Hokka Zakrisson
 | |
| short_description: Ensure a particular line is in a file, or replace an
 | |
|                    existing line using a back-referenced regular expression.
 | |
| description:
 | |
|   - This module will search a file for a line, and ensure that it is present or absent.
 | |
|   - This is primarily useful when you want to change a single line in a
 | |
|     file only. For other cases, see the M(copy) or M(template) modules.
 | |
| version_added: "0.7"
 | |
| options:
 | |
|   dest:
 | |
|     required: true
 | |
|     aliases: [ name, destfile ]
 | |
|     description:
 | |
|       - The file to modify.
 | |
|   regexp:
 | |
|     required: true
 | |
|     description:
 | |
|       - The regular expression to look for in every line of the file. For
 | |
|         C(state=present), the pattern to replace if found; only the last line
 | |
|         found will be replaced. For C(state=absent), the pattern of the line
 | |
|         to remove.  Uses Python regular expressions; see
 | |
|         U(http://docs.python.org/2/library/re.html).
 | |
|   state:
 | |
|     required: false
 | |
|     choices: [ present, absent ]
 | |
|     default: "present"
 | |
|     aliases: []
 | |
|     description:
 | |
|       - Whether the line should be there or not.
 | |
|   line:
 | |
|     required: false
 | |
|     description:
 | |
|       - Required for C(state=present). The line to insert/replace into the
 | |
|         file. If backrefs is set, may contain backreferences that will get
 | |
|         expanded with the regexp capture groups if the regexp matches. The
 | |
|         backreferences should be double escaped (see examples).
 | |
|   backrefs:
 | |
|     required: false
 | |
|     default: "no"
 | |
|     choices: [ "yes", "no" ]
 | |
|     version_added: "1.1"
 | |
|     description:
 | |
|       - Used with C(state=present). If set, line can contain backreferences
 | |
|         (both positional and named) that will get populated if the regexp
 | |
|         matches. This flag changes the operation of the module slightly;
 | |
|         insertbefore and insertafter will be ignored, and if the regexp
 | |
|         doesn't match anywhere in the file, the file will be left unchanged.
 | |
|         If the regexp does match, the last matching line will be replaced by
 | |
|         the expanded line parameter.
 | |
|   insertafter:
 | |
|     required: false
 | |
|     default: EOF
 | |
|     description:
 | |
|       - Used with C(state=present). If specified, the line will be inserted
 | |
|         after the specified regular expression. A special value is
 | |
|         available; C(EOF) for inserting the line at the end of the file.
 | |
|         May not be used with backrefs.
 | |
|     choices: [ 'EOF', '*regex*' ]
 | |
|   insertbefore:
 | |
|     required: false
 | |
|     version_added: "1.1"
 | |
|     description:
 | |
|       - Used with C(state=present). If specified, the line will be inserted
 | |
|         before the specified regular expression. A value is available;
 | |
|         C(BOF) for inserting the line at the beginning of the file.
 | |
|         May not be used with backrefs.
 | |
|     choices: [ 'BOF', '*regex*' ]
 | |
|   create:
 | |
|     required: false
 | |
|     choices: [ "yes", "no" ]
 | |
|     default: "no"
 | |
|     description:
 | |
|       - Used with C(state=present). If specified, the file will be created
 | |
|         if it does not already exist. By default it will fail if the file
 | |
|         is missing.
 | |
|   backup:
 | |
|      required: false
 | |
|      default: "no"
 | |
|      choices: [ "yes", "no" ]
 | |
|      description:
 | |
|        - Create a backup file including the timestamp information so you can
 | |
|          get the original file back if you somehow clobbered it incorrectly.
 | |
|   others:
 | |
|      description:
 | |
|        - All arguments accepted by the M(file) module also work here.
 | |
|      required: false
 | |
| """
 | |
| 
 | |
| EXAMPLES = r"""
 | |
| - lineinfile: dest=/etc/selinux/config regexp=^SELINUX= line=SELINUX=disabled
 | |
| 
 | |
| - lineinfile: dest=/etc/sudoers state=absent regexp="^%wheel"
 | |
| 
 | |
| - lineinfile: dest=/etc/hosts regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' owner=root group=root mode=0644
 | |
| 
 | |
| - lineinfile: dest=/etc/httpd/conf/httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080"
 | |
| 
 | |
| - lineinfile: dest=/etc/services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default"
 | |
| 
 | |
| # Fully quoted because of the ': ' on the line. See the Gotchas in the YAML docs.
 | |
| - lineinfile: "dest=/etc/sudoers state=present regexp='^%wheel' line='%wheel ALL=(ALL) NOPASSWD: ALL'"
 | |
| 
 | |
| - lineinfile: dest=/opt/jboss-as/bin/standalone.conf regexp='^(.*)Xms(\d+)m(.*)$' line='\1Xms${xms}m\3' backrefs=yes
 | |
| """
 | |
| 
 | |
| def write_changes(module,lines,dest):
 | |
| 
 | |
|     tmpfd, tmpfile = tempfile.mkstemp()
 | |
|     f = os.fdopen(tmpfd,'wb')
 | |
|     f.writelines(lines)
 | |
|     f.close()
 | |
| 
 | |
|     module.atomic_move(tmpfile, dest)
 | |
| 
 | |
| def check_file_attrs(module, changed, message):
 | |
| 
 | |
|     file_args = module.load_file_common_arguments(module.params)
 | |
|     if module.set_file_attributes_if_different(file_args, False):
 | |
| 
 | |
|         if changed:
 | |
|             message += " and "
 | |
|         changed = True
 | |
|         message += "ownership, perms or SE linux context changed"
 | |
| 
 | |
|     return message, changed
 | |
| 
 | |
| 
 | |
| def present(module, dest, regexp, line, insertafter, insertbefore, create,
 | |
|             backup, backrefs):
 | |
| 
 | |
|     if not os.path.exists(dest):
 | |
|         if not create:
 | |
|             module.fail_json(rc=257, msg='Destination %s does not exist !' % dest)
 | |
|         destpath = os.path.dirname(dest)
 | |
|         if not os.path.exists(destpath):
 | |
|             os.makedirs(destpath)
 | |
|         lines = []
 | |
|     else:
 | |
|         f = open(dest, 'rb')
 | |
|         lines = f.readlines()
 | |
|         f.close()
 | |
| 
 | |
|     msg = ""
 | |
| 
 | |
|     mre = re.compile(regexp)
 | |
| 
 | |
|     if insertafter not in (None, 'BOF', 'EOF'):
 | |
|         insre = re.compile(insertafter)
 | |
|     elif insertbefore not in (None, 'BOF'):
 | |
|         insre = re.compile(insertbefore)
 | |
|     else:
 | |
|         insre = None
 | |
| 
 | |
|     # index[0] is the line num where regexp has been found
 | |
|     # index[1] is the line num where insertafter/inserbefore has been found
 | |
|     index = [-1, -1]
 | |
|     m = None
 | |
|     for lineno, cur_line in enumerate(lines):
 | |
|         match_found = mre.search(cur_line)
 | |
|         if match_found:
 | |
|             index[0] = lineno
 | |
|             m = match_found
 | |
|         elif insre is not None and insre.search(cur_line):
 | |
|             if insertafter:
 | |
|                 # + 1 for the next line
 | |
|                 index[1] = lineno + 1
 | |
|             if insertbefore:
 | |
|                 # + 1 for the previous line
 | |
|                 index[1] = lineno
 | |
| 
 | |
|     msg = ''
 | |
|     changed = False
 | |
|     # Regexp matched a line in the file
 | |
|     if index[0] != -1:
 | |
|         if backrefs:
 | |
|             new_line = m.expand(line)
 | |
|         else:
 | |
|             # Don't do backref expansion if not asked.
 | |
|             new_line = line
 | |
| 
 | |
|         if lines[index[0]] != new_line + os.linesep:
 | |
|             lines[index[0]] = new_line + os.linesep
 | |
|             msg = 'line replaced'
 | |
|             changed = True
 | |
|     elif backrefs:
 | |
|         # Do absolutely nothing, since it's not safe generating the line
 | |
|         # without the regexp matching to populate the backrefs.
 | |
|         pass
 | |
|     # Add it to the beginning of the file
 | |
|     elif insertbefore == 'BOF' or insertafter == 'BOF':
 | |
|         lines.insert(0, line + os.linesep)
 | |
|         msg = 'line added'
 | |
|         changed = True
 | |
|     # Add it to the end of the file if requested or
 | |
|     # if insertafter=/insertbefore didn't match anything
 | |
|     # (so default behaviour is to add at the end)
 | |
|     elif insertafter == 'EOF':
 | |
|         lines.append(line + os.linesep)
 | |
|         msg = 'line added'
 | |
|         changed = True
 | |
|     # Do nothing if insert* didn't match
 | |
|     elif index[1] == -1:
 | |
|         pass
 | |
|     # insert* matched, but not the regexp
 | |
|     else:
 | |
|         lines.insert(index[1], line + os.linesep)
 | |
|         msg = 'line added'
 | |
|         changed = True
 | |
| 
 | |
|     if changed and not module.check_mode:
 | |
|         if backup and os.path.exists(dest):
 | |
|             module.backup_local(dest)
 | |
|         write_changes(module, lines, dest)
 | |
| 
 | |
|     msg, changed = check_file_attrs(module, changed, msg)
 | |
|     module.exit_json(changed=changed, msg=msg)
 | |
| 
 | |
| 
 | |
| def absent(module, dest, regexp, backup):
 | |
| 
 | |
|     if not os.path.exists(dest):
 | |
|         module.exit_json(changed=False, msg="file not present")
 | |
| 
 | |
|     msg = ""
 | |
| 
 | |
|     f = open(dest, 'rb')
 | |
|     lines = f.readlines()
 | |
|     f.close()
 | |
|     cre = re.compile(regexp)
 | |
|     found = []
 | |
| 
 | |
|     def matcher(line):
 | |
|         if cre.search(line):
 | |
|             found.append(line)
 | |
|             return False
 | |
|         else:
 | |
|             return True
 | |
| 
 | |
|     lines = filter(matcher, lines)
 | |
|     changed = len(found) > 0
 | |
|     if changed and not module.check_mode:
 | |
|         if backup:
 | |
|             module.backup_local(dest)
 | |
|         write_changes(module, lines, dest)
 | |
| 
 | |
|     if changed:
 | |
|         msg = "%s line(s) removed" % len(found)
 | |
| 
 | |
|     msg, changed = check_file_attrs(module, changed, msg)
 | |
|     module.exit_json(changed=changed, found=len(found), msg=msg)
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     module = AnsibleModule(
 | |
|         argument_spec=dict(
 | |
|             dest=dict(required=True, aliases=['name', 'destfile']),
 | |
|             state=dict(default='present', choices=['absent', 'present']),
 | |
|             regexp=dict(required=True),
 | |
|             line=dict(aliases=['value']),
 | |
|             insertafter=dict(default=None),
 | |
|             insertbefore=dict(default=None),
 | |
|             backrefs=dict(default=False, type='bool'),
 | |
|             create=dict(default=False, type='bool'),
 | |
|             backup=dict(default=False, type='bool'),
 | |
|         ),
 | |
|         mutually_exclusive=[['insertbefore', 'insertafter']],
 | |
|         add_file_common_args=True,
 | |
|         supports_check_mode=True
 | |
|     )
 | |
| 
 | |
|     params = module.params
 | |
|     create = module.params['create']
 | |
|     backup = module.params['backup']
 | |
|     backrefs = module.params['backrefs']
 | |
|     dest = os.path.expanduser(params['dest'])
 | |
| 
 | |
| 
 | |
|     if os.path.isdir(dest):
 | |
|         module.fail_json(rc=256, msg='Destination %s is a directory !' % dest)
 | |
| 
 | |
|     if params['state'] == 'present':
 | |
|         if params.get('line', None) is None:
 | |
|             module.fail_json(msg='line= is required with state=present')
 | |
| 
 | |
|         # Deal with the insertafter default value manually, to avoid errors
 | |
|         # because of the mutually_exclusive mechanism.
 | |
|         ins_bef, ins_aft = params['insertbefore'], params['insertafter']
 | |
|         if ins_bef is None and ins_aft is None:
 | |
|             ins_aft = 'EOF'
 | |
| 
 | |
|         present(module, dest, params['regexp'], params['line'],
 | |
|                 ins_aft, ins_bef, create, backup, backrefs)
 | |
|     else:
 | |
|         absent(module, dest, params['regexp'], backup)
 | |
| 
 | |
| # this is magic, see lib/ansible/module_common.py
 | |
| #<<INCLUDE_ANSIBLE_MODULE_COMMON>>
 | |
| 
 | |
| main()
 |