1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# Copyright: (c) 2016, Roman Belyakovsky <ihryamzik () gmail.com> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11ANSIBLE_METADATA = {'metadata_version': '1.1', 12 'status': ['stableinterface'], 13 'supported_by': 'community'} 14 15DOCUMENTATION = ''' 16--- 17module: interfaces_file 18short_description: Tweak settings in /etc/network/interfaces files 19extends_documentation_fragment: files 20description: 21 - Manage (add, remove, change) individual interface options in an interfaces-style file without having 22 to manage the file as a whole with, say, M(template) or M(assemble). Interface has to be presented in a file. 23 - Read information about interfaces from interfaces-styled files 24version_added: "2.4" 25options: 26 dest: 27 description: 28 - Path to the interfaces file 29 default: /etc/network/interfaces 30 iface: 31 description: 32 - Name of the interface, required for value changes or option remove 33 address_family: 34 description: 35 - Address family of the interface, useful if same interface name is used for both inet and inet6 36 version_added: "2.8" 37 option: 38 description: 39 - Name of the option, required for value changes or option remove 40 value: 41 description: 42 - If I(option) is not presented for the I(interface) and I(state) is C(present) option will be added. 43 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. 44 C(pre-up), C(up), C(post-up) and C(down) options can't be updated, only adding new options, removing existing 45 ones or cleaning the whole option set are supported 46 backup: 47 description: 48 - Create a backup file including the timestamp information so you can get 49 the original file back if you somehow clobbered it incorrectly. 50 type: bool 51 default: 'no' 52 state: 53 description: 54 - If set to C(absent) the option or section will be removed if present instead of created. 55 default: "present" 56 choices: [ "present", "absent" ] 57 58notes: 59 - If option is defined multiple times last one will be updated but all will be deleted in case of an absent state 60requirements: [] 61author: "Roman Belyakovsky (@hryamzik)" 62''' 63 64RETURN = ''' 65dest: 66 description: destination file/path 67 returned: success 68 type: str 69 sample: "/etc/network/interfaces" 70ifaces: 71 description: interfaces dictionary 72 returned: success 73 type: complex 74 contains: 75 ifaces: 76 description: interface dictionary 77 returned: success 78 type: dict 79 contains: 80 eth0: 81 description: Name of the interface 82 returned: success 83 type: dict 84 contains: 85 address_family: 86 description: interface address family 87 returned: success 88 type: str 89 sample: "inet" 90 method: 91 description: interface method 92 returned: success 93 type: str 94 sample: "manual" 95 mtu: 96 description: other options, all values returned as strings 97 returned: success 98 type: str 99 sample: "1500" 100 pre-up: 101 description: list of C(pre-up) scripts 102 returned: success 103 type: list 104 sample: 105 - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" 106 - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" 107 up: 108 description: list of C(up) scripts 109 returned: success 110 type: list 111 sample: 112 - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" 113 - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" 114 post-up: 115 description: list of C(post-up) scripts 116 returned: success 117 type: list 118 sample: 119 - "route add -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" 120 - "route add -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" 121 down: 122 description: list of C(down) scripts 123 returned: success 124 type: list 125 sample: 126 - "route del -net 10.10.10.0/24 gw 10.10.10.1 dev eth1" 127 - "route del -net 10.10.11.0/24 gw 10.10.11.1 dev eth2" 128... 129''' 130 131EXAMPLES = ''' 132# Set eth1 mtu configuration value to 8000 133- interfaces_file: 134 dest: /etc/network/interfaces.d/eth1.cfg 135 iface: eth1 136 option: mtu 137 value: 8000 138 backup: yes 139 state: present 140 register: eth1_cfg 141''' 142 143import os 144import re 145import tempfile 146 147from ansible.module_utils.basic import AnsibleModule 148from ansible.module_utils._text import to_bytes 149 150 151def lineDict(line): 152 return {'line': line, 'line_type': 'unknown'} 153 154 155def optionDict(line, iface, option, value, address_family): 156 return {'line': line, 'iface': iface, 'option': option, 'value': value, 'line_type': 'option', 'address_family': address_family} 157 158 159def getValueFromLine(s): 160 spaceRe = re.compile(r'\s+') 161 for m in spaceRe.finditer(s): 162 pass 163 valueEnd = m.start() 164 option = s.split()[0] 165 optionStart = s.find(option) 166 optionLen = len(option) 167 valueStart = re.search(r'\s', s[optionLen + optionStart:]).end() + optionLen + optionStart 168 return s[valueStart:valueEnd] 169 170 171def read_interfaces_file(module, filename): 172 f = open(filename, 'r') 173 return read_interfaces_lines(module, f) 174 175 176def read_interfaces_lines(module, line_strings): 177 lines = [] 178 ifaces = {} 179 currently_processing = None 180 i = 0 181 for line in line_strings: 182 i += 1 183 words = line.split() 184 if len(words) < 1: 185 lines.append(lineDict(line)) 186 continue 187 if words[0][0] == "#": 188 lines.append(lineDict(line)) 189 continue 190 if words[0] == "mapping": 191 # currmap = calloc(1, sizeof *currmap); 192 lines.append(lineDict(line)) 193 currently_processing = "MAPPING" 194 elif words[0] == "source": 195 lines.append(lineDict(line)) 196 currently_processing = "NONE" 197 elif words[0] == "source-dir": 198 lines.append(lineDict(line)) 199 currently_processing = "NONE" 200 elif words[0] == "source-directory": 201 lines.append(lineDict(line)) 202 currently_processing = "NONE" 203 elif words[0] == "iface": 204 currif = { 205 "pre-up": [], 206 "up": [], 207 "down": [], 208 "post-up": [] 209 } 210 iface_name = words[1] 211 try: 212 currif['address_family'] = words[2] 213 except IndexError: 214 currif['address_family'] = None 215 address_family = currif['address_family'] 216 try: 217 currif['method'] = words[3] 218 except IndexError: 219 currif['method'] = None 220 221 ifaces[iface_name] = currif 222 lines.append({'line': line, 'iface': iface_name, 'line_type': 'iface', 'params': currif, 'address_family': address_family}) 223 currently_processing = "IFACE" 224 elif words[0] == "auto": 225 lines.append(lineDict(line)) 226 currently_processing = "NONE" 227 elif words[0].startswith("allow-"): 228 lines.append(lineDict(line)) 229 currently_processing = "NONE" 230 elif words[0] == "no-auto-down": 231 lines.append(lineDict(line)) 232 currently_processing = "NONE" 233 elif words[0] == "no-scripts": 234 lines.append(lineDict(line)) 235 currently_processing = "NONE" 236 else: 237 if currently_processing == "IFACE": 238 option_name = words[0] 239 # TODO: if option_name in currif.options 240 value = getValueFromLine(line) 241 lines.append(optionDict(line, iface_name, option_name, value, address_family)) 242 if option_name in ["pre-up", "up", "down", "post-up"]: 243 currif[option_name].append(value) 244 else: 245 currif[option_name] = value 246 elif currently_processing == "MAPPING": 247 lines.append(lineDict(line)) 248 elif currently_processing == "NONE": 249 lines.append(lineDict(line)) 250 else: 251 module.fail_json(msg="misplaced option %s in line %d" % (line, i)) 252 return None, None 253 return lines, ifaces 254 255 256def setInterfaceOption(module, lines, iface, option, raw_value, state, address_family=None): 257 value = str(raw_value) 258 changed = False 259 260 iface_lines = [item for item in lines if "iface" in item and item["iface"] == iface] 261 if address_family is not None: 262 iface_lines = [item for item in iface_lines 263 if "address_family" in item and item["address_family"] == address_family] 264 265 if len(iface_lines) < 1: 266 # interface not found 267 module.fail_json(msg="Error: interface %s not found" % iface) 268 return changed, None 269 270 iface_options = list(filter(lambda i: i['line_type'] == 'option', iface_lines)) 271 target_options = list(filter(lambda i: i['option'] == option, iface_options)) 272 273 if state == "present": 274 if len(target_options) < 1: 275 changed = True 276 # add new option 277 last_line_dict = iface_lines[-1] 278 changed, lines = addOptionAfterLine(option, value, iface, lines, last_line_dict, iface_options, address_family) 279 else: 280 if option in ["pre-up", "up", "down", "post-up"]: 281 if len(list(filter(lambda i: i['value'] == value, target_options))) < 1: 282 changed, lines = addOptionAfterLine(option, value, iface, lines, target_options[-1], iface_options, address_family) 283 else: 284 # if more than one option found edit the last one 285 if target_options[-1]['value'] != value: 286 changed = True 287 target_option = target_options[-1] 288 old_line = target_option['line'] 289 old_value = target_option['value'] 290 address_family = target_option['address_family'] 291 prefix_start = old_line.find(option) 292 optionLen = len(option) 293 old_value_position = re.search(r"\s+".join(map(re.escape, old_value.split())), old_line[prefix_start + optionLen:]) 294 start = old_value_position.start() + prefix_start + optionLen 295 end = old_value_position.end() + prefix_start + optionLen 296 line = old_line[:start] + value + old_line[end:] 297 index = len(lines) - lines[::-1].index(target_option) - 1 298 lines[index] = optionDict(line, iface, option, value, address_family) 299 elif state == "absent": 300 if len(target_options) >= 1: 301 if option in ["pre-up", "up", "down", "post-up"] and value is not None and value != "None": 302 for target_option in filter(lambda i: i['value'] == value, target_options): 303 changed = True 304 lines = list(filter(lambda ln: ln != target_option, lines)) 305 else: 306 changed = True 307 for target_option in target_options: 308 lines = list(filter(lambda ln: ln != target_option, lines)) 309 else: 310 module.fail_json(msg="Error: unsupported state %s, has to be either present or absent" % state) 311 312 return changed, lines 313 314 315def addOptionAfterLine(option, value, iface, lines, last_line_dict, iface_options, address_family): 316 # Changing method of interface is not an addition 317 if option == 'method': 318 changed = False 319 for ln in lines: 320 if ln.get('line_type', '') == 'iface' and ln.get('iface', '') == iface and value != ln.get('params', {}).get('method', ''): 321 changed = True 322 ln['line'] = re.sub(ln.get('params', {}).get('method', '') + '$', value, ln.get('line')) 323 ln['params']['method'] = value 324 return changed, lines 325 326 last_line = last_line_dict['line'] 327 prefix_start = last_line.find(last_line.split()[0]) 328 suffix_start = last_line.rfind(last_line.split()[-1]) + len(last_line.split()[-1]) 329 prefix = last_line[:prefix_start] 330 331 if len(iface_options) < 1: 332 # interface has no options, ident 333 prefix += " " 334 335 line = prefix + "%s %s" % (option, value) + last_line[suffix_start:] 336 option_dict = optionDict(line, iface, option, value, address_family) 337 index = len(lines) - lines[::-1].index(last_line_dict) 338 lines.insert(index, option_dict) 339 return True, lines 340 341 342def write_changes(module, lines, dest): 343 344 tmpfd, tmpfile = tempfile.mkstemp() 345 f = os.fdopen(tmpfd, 'wb') 346 f.write(to_bytes(''.join(lines), errors='surrogate_or_strict')) 347 f.close() 348 module.atomic_move(tmpfile, os.path.realpath(dest)) 349 350 351def main(): 352 module = AnsibleModule( 353 argument_spec=dict( 354 dest=dict(type='path', default='/etc/network/interfaces'), 355 iface=dict(type='str'), 356 address_family=dict(type='str'), 357 option=dict(type='str'), 358 value=dict(type='str'), 359 backup=dict(type='bool', default=False), 360 state=dict(type='str', default='present', choices=['absent', 'present']), 361 ), 362 add_file_common_args=True, 363 supports_check_mode=True, 364 required_by=dict( 365 option=('iface',), 366 ), 367 ) 368 369 dest = module.params['dest'] 370 iface = module.params['iface'] 371 address_family = module.params['address_family'] 372 option = module.params['option'] 373 value = module.params['value'] 374 backup = module.params['backup'] 375 state = module.params['state'] 376 377 if option is not None and state == "present" and value is None: 378 module.fail_json(msg="Value must be set if option is defined and state is 'present'") 379 380 lines, ifaces = read_interfaces_file(module, dest) 381 382 changed = False 383 384 if option is not None: 385 changed, lines = setInterfaceOption(module, lines, iface, option, value, state, address_family) 386 387 if changed: 388 _, ifaces = read_interfaces_lines(module, [d['line'] for d in lines if 'line' in d]) 389 390 if changed and not module.check_mode: 391 if backup: 392 module.backup_local(dest) 393 write_changes(module, [d['line'] for d in lines if 'line' in d], dest) 394 395 module.exit_json(dest=dest, changed=changed, ifaces=ifaces) 396 397 398if __name__ == '__main__': 399 main() 400