lineinfile 12.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#!/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
23
import tempfile
24

25
DOCUMENTATION = """
26 27
---
module: lineinfile
28
author: Daniel Hokka Zakrisson
29 30
short_description: Ensure a particular line is in a file, or replace an
                   existing line using a back-referenced regular expression.
31 32 33 34 35 36
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:
37
  dest:
38
    required: true
39
    aliases: [ name, destfile ]
40
    description:
41
      - The file to modify.
42
  regexp:
43
    required: false
44
    description:
45 46 47
      - 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
48 49
        to remove.  Uses Python regular expressions; see
        U(http://docs.python.org/2/library/re.html).
50 51 52 53 54 55 56 57 58 59
  state:
    required: false
    choices: [ present, absent ]
    default: "present"
    aliases: []
    description:
      - Whether the line should be there or not.
  line:
    required: false
    description:
60
      - Required for C(state=present). The line to insert/replace into the
61 62 63 64 65 66 67
        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" ]
68
    version_added: "1.1"
69 70 71 72
    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;
73
        insertbefore and insertafter will be ignored, and if the regexp
74 75 76
        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.
77 78 79 80
  insertafter:
    required: false
    default: EOF
    description:
81
      - Used with C(state=present). If specified, the line will be inserted
82 83
        after the specified regular expression. A special value is
        available; C(EOF) for inserting the line at the end of the file.
84
        May not be used with backrefs.
85
    choices: [ 'EOF', '*regex*' ]
86 87
  insertbefore:
    required: false
88
    version_added: "1.1"
89 90 91
    description:
      - Used with C(state=present). If specified, the line will be inserted
        before the specified regular expression. A value is available;
92 93
        C(BOF) for inserting the line at the beginning of the file.
        May not be used with backrefs.
94
    choices: [ 'BOF', '*regex*' ]
95 96
  create:
    required: false
97 98
    choices: [ "yes", "no" ]
    default: "no"
99 100 101 102
    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.
103 104
  backup:
     required: false
105 106
     default: "no"
     choices: [ "yes", "no" ]
107 108 109
     description:
       - Create a backup file including the timestamp information so you can
         get the original file back if you somehow clobbered it incorrectly.
110 111 112 113 114 115 116
  validate:
     required: false
     description:
       - validation to run before copying into place
     required: false
     default: None
     version_added: "1.4"
117 118
  others:
     description:
tin committed
119
       - All arguments accepted by the M(file) module also work here.
120
     required: false
121 122
"""

123
EXAMPLES = r"""
124
- lineinfile: dest=/etc/selinux/config regexp=^SELINUX= line=SELINUX=disabled
125

126
- lineinfile: dest=/etc/sudoers state=absent regexp="^%wheel"
127

128
- lineinfile: dest=/etc/hosts regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' owner=root group=root mode=0644
129

130
- lineinfile: dest=/etc/httpd/conf/httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080"
131

132
- lineinfile: dest=/etc/services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default"
133

134 135 136
# Add a line to a file if it does not exist, without passing regexp
- lineinfile: dest=/tmp/testfile line="192.168.1.99 foo.lab.net foo"

137 138
# 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'"
139

140
- lineinfile: dest=/opt/jboss-as/bin/standalone.conf regexp='^(.*)Xms(\d+)m(.*)$' line='\1Xms${xms}m\3' backrefs=yes
141 142 143

# Validate a the sudoers file before saving
- lineinfile: dest=/etc/sudoers state=present regexp='^%ADMIN ALL\=' line='%ADMIN ALL=(ALL) NOPASSWD:ALL' validate='visudo -cf %s'
144
"""
tin committed
145

146 147 148 149 150 151 152
def write_changes(module,lines,dest):

    tmpfd, tmpfile = tempfile.mkstemp()
    f = os.fdopen(tmpfd,'wb')
    f.writelines(lines)
    f.close()

153 154 155 156 157 158 159 160 161 162
    validate = module.params.get('validate', None)
    valid = not validate
    if validate:
        (rc, out, err) = module.run_command(validate % tmpfile)
        valid = rc == 0
        if rc != 0:
            module.fail_json(msg='failed to validate: '
                                 'rc:%s error:%s' % (rc,err))
    if valid:
        module.atomic_move(tmpfile, dest)
163

Michel Blanc committed
164
def check_file_attrs(module, changed, message):
165 166 167

    file_args = module.load_file_common_arguments(module.params)
    if module.set_file_attributes_if_different(file_args, False):
Michel Blanc committed
168

169 170 171 172 173
        if changed:
            message += " and "
        changed = True
        message += "ownership, perms or SE linux context changed"

tin committed
174 175
    return message, changed

176

tin committed
177 178
def present(module, dest, regexp, line, insertafter, insertbefore, create,
            backup, backrefs):
179

180
    if not os.path.exists(dest):
181 182 183 184 185 186 187 188 189 190
        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()
191

Michel Blanc committed
192 193
    msg = ""

194 195
    if regexp is not None:
        mre = re.compile(regexp)
tin committed
196 197

    if insertafter not in (None, 'BOF', 'EOF'):
198
        insre = re.compile(insertafter)
tin committed
199
    elif insertbefore not in (None, 'BOF'):
200 201 202
        insre = re.compile(insertbefore)
    else:
        insre = None
203

204 205
    # index[0] is the line num where regexp has been found
    # index[1] is the line num where insertafter/inserbefore has been found
206
    index = [-1, -1]
207
    m = None
tin committed
208
    for lineno, cur_line in enumerate(lines):
209 210 211 212
        if regexp is not None:
            match_found = mre.search(cur_line)
        else:
            match_found = line == cur_line.rstrip('\r\n')
213
        if match_found:
214
            index[0] = lineno
215
            m = match_found
tin committed
216
        elif insre is not None and insre.search(cur_line):
217 218 219 220 221 222
            if insertafter:
                # + 1 for the next line
                index[1] = lineno + 1
            if insertbefore:
                # + 1 for the previous line
                index[1] = lineno
223

tin committed
224 225
    msg = ''
    changed = False
226 227
    # Regexp matched a line in the file
    if index[0] != -1:
tin committed
228 229
        if backrefs:
            new_line = m.expand(line)
230
        else:
tin committed
231 232 233 234 235
            # 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
236 237
            msg = 'line replaced'
            changed = True
tin committed
238 239 240 241
    elif backrefs:
        # Do absolutely nothing, since it's not safe generating the line
        # without the regexp matching to populate the backrefs.
        pass
242
    # Add it to the beginning of the file
243
    elif insertbefore == 'BOF' or insertafter == 'BOF':
244 245 246
        lines.insert(0, line + os.linesep)
        msg = 'line added'
        changed = True
247 248 249
    # 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)
250
    elif insertafter == 'EOF':
251 252 253
        lines.append(line + os.linesep)
        msg = 'line added'
        changed = True
tin committed
254
    # Do nothing if insert* didn't match
255
    elif index[1] == -1:
tin committed
256 257
        pass
    # insert* matched, but not the regexp
258 259 260 261 262
    else:
        lines.insert(index[1], line + os.linesep)
        msg = 'line added'
        changed = True

263
    if changed and not module.check_mode:
264
        if backup and os.path.exists(dest):
265
            module.backup_local(dest)
266
        write_changes(module, lines, dest)
267

tin committed
268
    msg, changed = check_file_attrs(module, changed, msg)
269 270
    module.exit_json(changed=changed, msg=msg)

tin committed
271

272
def absent(module, dest, regexp, line, backup):
273

274
    if not os.path.exists(dest):
275 276 277
        module.exit_json(changed=False, msg="file not present")

    msg = ""
Michel Blanc committed
278

279
    f = open(dest, 'rb')
280 281
    lines = f.readlines()
    f.close()
282 283
    if regexp is not None:
        cre = re.compile(regexp)
284
    found = []
Michael DeHaan committed
285

286 287 288
    def matcher(cur_line):
        if regexp is not None:
            match_found = cre.search(cur_line)
289
        else:
290 291 292 293
            match_found = line == cur_line.rstrip('\r\n')
        if match_found:
            found.append(cur_line)
        return not match_found
Michael DeHaan committed
294

295 296
    lines = filter(matcher, lines)
    changed = len(found) > 0
297
    if changed and not module.check_mode:
298
        if backup:
299
            module.backup_local(dest)
300
        write_changes(module, lines, dest)
301 302 303 304

    if changed:
        msg = "%s line(s) removed" % len(found)

tin committed
305
    msg, changed = check_file_attrs(module, changed, msg)
306
    module.exit_json(changed=changed, found=len(found), msg=msg)
307

tin committed
308

309 310
def main():
    module = AnsibleModule(
tin committed
311
        argument_spec=dict(
312
            dest=dict(required=True, aliases=['name', 'destfile']),
313
            state=dict(default='present', choices=['absent', 'present']),
314
            regexp=dict(default=None),
315
            line=dict(aliases=['value']),
316 317
            insertafter=dict(default=None),
            insertbefore=dict(default=None),
tin committed
318
            backrefs=dict(default=False, type='bool'),
319 320
            create=dict(default=False, type='bool'),
            backup=dict(default=False, type='bool'),
321
            validate=dict(default=None, type='str'),
322
        ),
tin committed
323 324 325
        mutually_exclusive=[['insertbefore', 'insertafter']],
        add_file_common_args=True,
        supports_check_mode=True
326 327 328
    )

    params = module.params
329 330
    create = module.params['create']
    backup = module.params['backup']
tin committed
331 332
    backrefs = module.params['backrefs']
    dest = os.path.expanduser(params['dest'])
333

334 335 336 337

    if os.path.isdir(dest):
        module.fail_json(rc=256, msg='Destination %s is a directory !' % dest)

338
    if params['state'] == 'present':
339 340 341
        if backrefs and params['regexp'] is None:
            module.fail_json(msg='regexp= is required with backrefs=true')

342
        if params.get('line', None) is None:
343
            module.fail_json(msg='line= is required with state=present')
tin committed
344 345 346 347 348 349 350

        # 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'

351
        present(module, dest, params['regexp'], params['line'],
tin committed
352
                ins_aft, ins_bef, create, backup, backrefs)
353
    else:
354 355 356 357
        if params['regexp'] is None and params.get('line', None) is None:
            module.fail_json(msg='one of line= or regexp= is required with state=absent')

        absent(module, dest, params['regexp'], params.get('line', None), backup)
358

359
# import module snippets
360
from ansible.module_utils.basic import *
361 362

main()