community.general/lib/ansible/modules/system/interfaces_file.py
Olivier Bourdon 612d0d6634 Fix interfaces_file for proper file contents (#37818)
The generated file was completely unusable by the system
therefore the fix which ensures that diffing the file
prior to changes and after only shows diffs

Furthermore the code did not work for Python 3.6
>       f.writelines(to_bytes(lines, errors='surrogate_or_strict'))
E       TypeError: a bytes-like object is required, not 'int'

The other modifications (lambda variable renaming) is to
comply with default flake8 rules
2018-03-28 10:21:43 +01:00

376 lines
13 KiB
Python
Executable file

#!/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] == "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] == "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):
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()