summaryrefslogtreecommitdiff
path: root/lib/ansible/modules/system/interfaces_file.py
blob: 389dcb770eb811948e0cf8894686b91d2d044e96 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2016, Roman Belyakovsky <ihryamzik () gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['stableinterface'],
                    'supported_by': 'community'}

DOCUMENTATION = '''
---
module: interfaces_file
short_description: Tweak settings in /etc/network/interfaces files
extends_documentation_fragment: files
description:
     - Manage (add, remove, change) individual interface options in an interfaces-style file without having
       to manage the file as a whole with, say, M(template) or M(assemble). Interface has to be presented in a file.
     - Read information about interfaces from interfaces-styled files
version_added: "2.4"
options:
  dest:
    description:
      - Path to the interfaces file
    default: /etc/network/interfaces
  iface:
    description:
      - Name of the interface, required for value changes or option remove
  option:
    description:
      - Name of the option, required for value changes or option remove
  value:
    description:
      - If I(option) is not presented for the I(interface) and I(state) is C(present) option will be added.
        If I(option) already exists and is not C(pre-up), C(up), C(post-up) or C(down), it's value will be updated.
        C(pre-up), C(up), C(post-up) and C(down) options can't be updated, only adding new options, removing existing
        ones or cleaning the whole option set are supported
  backup:
    description:
      - Create a backup file including the timestamp information so you can get
        the original file back if you somehow clobbered it incorrectly.
    type: bool
    default: 'no'
  state:
    description:
      - If set to C(absent) the option or section will be removed if present instead of created.
    default: "present"
    choices: [ "present", "absent" ]

notes:
   - If option is defined multiple times last one will be updated but all will be deleted in case of an absent state
requirements: []
author: "Roman Belyakovsky (@hryamzik)"
'''

RETURN = '''
dest:
    description: destination file/path
    returned: success
    type: string
    sample: "/etc/network/interfaces"
ifaces:
    description: interfaces dictionary
    returned: success
    type: complex
    contains:
      ifaces:
        description: interface dictionary
        returned: success
        type: dictionary
        contains:
          eth0:
            description: Name of the interface
            returned: success
            type: dictionary
            contains:
              address_family:
                description: interface address family
                returned: success
                type: string
                sample: "inet"
              method:
                description: interface method
                returned: success
                type: string
                sample: "manual"
              mtu:
                description: other options, all values returned as strings
                returned: success
                type: string
                sample: "1500"
              pre-up:
                description: list of C(pre-up) scripts
                returned: success
                type: list
                sample:
                  - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1"
                  - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2"
              up:
                description: list of C(up) scripts
                returned: success
                type: list
                sample:
                  - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1"
                  - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2"
              post-up:
                description: list of C(post-up) scripts
                returned: success
                type: list
                sample:
                  - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1"
                  - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2"
              down:
                description: list of C(down) scripts
                returned: success
                type: list
                sample:
                  - "route del -net 10.10.10.0/24 gw 10.10.10.1 dev eth1"
                  - "route del -net 10.10.11.0/24 gw 10.10.11.1 dev eth2"
...
'''

EXAMPLES = '''
# Set eth1 mtu configuration value to 8000
- interfaces_file:
    dest: /etc/network/interfaces.d/eth1.cfg
    iface: eth1
    option: mtu
    value: 8000
    backup: yes
    state: present
  register: eth1_cfg
'''

import os
import re
import tempfile

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes


def lineDict(line):
    return {'line': line, 'line_type': 'unknown'}


def optionDict(line, iface, option, value):
    return {'line': line, 'iface': iface, 'option': option, 'value': value, 'line_type': 'option'}


def getValueFromLine(s):
    spaceRe = re.compile(r'\s+')
    for m in spaceRe.finditer(s):
        pass
    valueEnd = m.start()
    option = s.split()[0]
    optionStart = s.find(option)
    optionLen = len(option)
    valueStart = re.search(r'\s', s[optionLen + optionStart:]).end() + optionLen + optionStart
    return s[valueStart:valueEnd]


def read_interfaces_file(module, filename):
    f = open(filename, 'r')
    return read_interfaces_lines(module, f)


def read_interfaces_lines(module, line_strings):
    lines = []
    ifaces = {}
    currently_processing = None
    i = 0
    for line in line_strings:
        i += 1
        words = line.split()
        if len(words) < 1:
            lines.append(lineDict(line))
            continue
        if words[0][0] == "#":
            lines.append(lineDict(line))
            continue
        if words[0] == "mapping":
            # currmap = calloc(1, sizeof *currmap);
            lines.append(lineDict(line))
            currently_processing = "MAPPING"
        elif words[0] == "source":
            lines.append(lineDict(line))
            currently_processing = "NONE"
        elif words[0] == "source-dir":
            lines.append(lineDict(line))
            currently_processing = "NONE"
        elif words[0] == "source-directory":
            lines.append(lineDict(line))
            currently_processing = "NONE"
        elif words[0] == "iface":
            currif = {
                "pre-up": [],
                "up": [],
                "down": [],
                "post-up": []
            }
            iface_name = words[1]
            try:
                currif['address_family'] = words[2]
            except IndexError:
                currif['address_family'] = None
            try:
                currif['method'] = words[3]
            except IndexError:
                currif['method'] = None

            ifaces[iface_name] = currif
            lines.append({'line': line, 'iface': iface_name, 'line_type': 'iface', 'params': currif})
            currently_processing = "IFACE"
        elif words[0] == "auto":
            lines.append(lineDict(line))
            currently_processing = "NONE"
        elif words[0].startswith("allow-"):
            lines.append(lineDict(line))
            currently_processing = "NONE"
        elif words[0] == "no-auto-down":
            lines.append(lineDict(line))
            currently_processing = "NONE"
        elif words[0] == "no-scripts":
            lines.append(lineDict(line))
            currently_processing = "NONE"
        else:
            if currently_processing == "IFACE":
                option_name = words[0]
                # TODO: if option_name in currif.options
                value = getValueFromLine(line)
                lines.append(optionDict(line, iface_name, option_name, value))
                if option_name in ["pre-up", "up", "down", "post-up"]:
                    currif[option_name].append(value)
                else:
                    currif[option_name] = value
            elif currently_processing == "MAPPING":
                lines.append(lineDict(line))
            elif currently_processing == "NONE":
                lines.append(lineDict(line))
            else:
                module.fail_json(msg="misplaced option %s in line %d" % (line, i))
                return None, None
    return lines, ifaces


def setInterfaceOption(module, lines, iface, option, raw_value, state):
    value = str(raw_value)
    changed = False

    iface_lines = [item for item in lines if "iface" in item and item["iface"] == iface]

    if len(iface_lines) < 1:
        # interface not found
        module.fail_json(msg="Error: interface %s not found" % iface)
        return changed

    iface_options = list(filter(lambda i: i['line_type'] == 'option', iface_lines))
    target_options = list(filter(lambda i: i['option'] == option, iface_options))

    if state == "present":
        if len(target_options) < 1:
            changed = True
            # add new option
            last_line_dict = iface_lines[-1]
            lines = addOptionAfterLine(option, value, iface, lines, last_line_dict, iface_options)
        else:
            if option in ["pre-up", "up", "down", "post-up"]:
                if len(list(filter(lambda i: i['value'] == value, target_options))) < 1:
                    changed = True
                    lines = addOptionAfterLine(option, value, iface, lines, target_options[-1], iface_options)
            else:
                # if more than one option found edit the last one
                if target_options[-1]['value'] != value:
                    changed = True
                    target_option = target_options[-1]
                    old_line = target_option['line']
                    old_value = target_option['value']
                    prefix_start = old_line.find(option)
                    optionLen = len(option)
                    old_value_position = re.search(r"\s+".join(old_value.split()), old_line[prefix_start + optionLen:])
                    start = old_value_position.start() + prefix_start + optionLen
                    end = old_value_position.end() + prefix_start + optionLen
                    line = old_line[:start] + value + old_line[end:]
                    index = len(lines) - lines[::-1].index(target_option) - 1
                    lines[index] = optionDict(line, iface, option, value)
    elif state == "absent":
        if len(target_options) >= 1:
            if option in ["pre-up", "up", "down", "post-up"] and value is not None and value != "None":
                for target_option in filter(lambda i: i['value'] == value, target_options):
                    changed = True
                    lines = list(filter(lambda ln: ln != target_option, lines))
            else:
                changed = True
                for target_option in target_options:
                    lines = list(filter(lambda ln: ln != target_option, lines))
    else:
        module.fail_json(msg="Error: unsupported state %s, has to be either present or absent" % state)

    return changed, lines


def addOptionAfterLine(option, value, iface, lines, last_line_dict, iface_options):
    # Changing method of interface is not an addition
    if option == 'method':
        for ln in lines:
            if ln.get('line_type', '') == 'iface' and ln.get('iface', '') == iface:
                ln['line'] = re.sub(ln.get('params', {}).get('method', '') + '$', value, ln.get('line'))
                ln['params']['method'] = value
        return lines

    last_line = last_line_dict['line']
    prefix_start = last_line.find(last_line.split()[0])
    suffix_start = last_line.rfind(last_line.split()[-1]) + len(last_line.split()[-1])
    prefix = last_line[:prefix_start]

    if len(iface_options) < 1:
        # interface has no options, ident
        prefix += "    "

    line = prefix + "%s %s" % (option, value) + last_line[suffix_start:]
    option_dict = optionDict(line, iface, option, value)
    index = len(lines) - lines[::-1].index(last_line_dict)
    lines.insert(index, option_dict)
    return lines


def write_changes(module, lines, dest):

    tmpfd, tmpfile = tempfile.mkstemp()
    f = os.fdopen(tmpfd, 'wb')
    f.write(to_bytes(''.join(lines), errors='surrogate_or_strict'))
    f.close()
    module.atomic_move(tmpfile, os.path.realpath(dest))


def main():
    module = AnsibleModule(
        argument_spec=dict(
            dest=dict(default='/etc/network/interfaces', required=False, type='path'),
            iface=dict(required=False),
            option=dict(required=False),
            value=dict(required=False),
            backup=dict(default='no', type='bool'),
            state=dict(default='present', choices=['present', 'absent']),
        ),
        add_file_common_args=True,
        supports_check_mode=True
    )

    dest = module.params['dest']
    iface = module.params['iface']
    option = module.params['option']
    value = module.params['value']
    backup = module.params['backup']
    state = module.params['state']

    if option is not None and iface is None:
        module.fail_json(msg="Inteface must be set if option is defined")

    if option is not None and state == "present" and value is None:
        module.fail_json(msg="Value must be set if option is defined and state is 'present'")

    lines, ifaces = read_interfaces_file(module, dest)

    changed = False

    if option is not None:
        changed, lines = setInterfaceOption(module, lines, iface, option, value, state)

    if changed:
        _, ifaces = read_interfaces_lines(module, [d['line'] for d in lines if 'line' in d])

    if changed and not module.check_mode:
        if backup:
            module.backup_local(dest)
        write_changes(module, [d['line'] for d in lines if 'line' in d], dest)

    module.exit_json(dest=dest, changed=changed, ifaces=ifaces)


if __name__ == '__main__':
    main()