blob: 5d24c02a742fa94475b50a6756cd5e00a33ea834 [file] [log] [blame]
David K. Bainbridgea677d4e2016-09-11 20:01:32 -07001#!/usr/bin/env python
2
3import json
4import os
5import re
6import sys
7import shlex
8import string
9import ipaddress
David K. Bainbridge5b392b82016-11-15 12:31:14 -080010# WANT_JSON
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070011
12# Regular expressions to identify comments and blank lines
13comment = re.compile("^\s*#")
14blank = re.compile("^\s*$")
15
16#####
17# Parsers
18#
19# Parses are methods that take the form 'parse_<keyword>', where the keyword
20# is the first word on a line in file. The purpose of the parser is to
21# evaluate the line tna update the interface model accordingly.
22####
23
24# Compares the current and desired network configuration to see if there
25# is a change and returns:
26# 0 if no change
27# -1 if the change has no semantic value (i.e. comments differ)
28# 1 if there is a semantic change (i.e. its meaningful)
29# the highest priority of change is returned, i.e. if there is both
30# a semantic and non-semantic change a 1 is returned indicating a
31# semantic change.
32def compare(have, want):
33 result = 0
34 for key in list(set().union(have.keys(), want.keys())):
35 if key in have.keys() and key in want.keys():
36 if have[key] != want[key]:
37 if key in ["description"]:
38 result = -1
39 else:
40 return 1
41 else:
42 if key in ["description"]:
43 result = -1
44 else:
45 return 1
46 return result
47
48# Creates an interface definition in the model and sets the auto
49# configuration to true
50def parse_auto(data, current, words, description):
51 if words[1] in data.keys():
52 iface = data[words[1]]
53 else:
54 iface = {}
55
56 if len(description) > 0:
57 iface["description"] = description
58
David K. Bainbridge5b392b82016-11-15 12:31:14 -080059 iface["auto"] = True
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070060 data[words[1]] = iface
61 return words[1]
62
63# Creates an interface definition in the model if one does not exist and
64# sets the type and configuation method
65def parse_iface(data, current, words, description):
66 if words[1] in data.keys():
67 iface = data[words[1]]
68 else:
69 iface = {}
70
71 if len(description) > 0:
72 iface["description"] = description
73
74 iface["type"] = words[2]
75 iface["config"] = words[3]
76 data[words[1]] = iface
77 return words[1]
78
David K. Bainbridge5b392b82016-11-15 12:31:14 -080079allow_lists = ["pre-up", "post-up", "pre-down", "post-down"]
80
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070081# Used to evaluate attributes and add a generic name / value pair to the interface
82# model
83def parse_add_attr(data, current, words, description):
David K. Bainbridge5b392b82016-11-15 12:31:14 -080084 global allow_lists
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070085 if current == "":
86 raise SyntaxError("Attempt to add attribute '%s' without an interface" % words[0])
87
88 if current in data.keys():
89 iface = data[current]
90 else:
91 iface = {}
92
93 if len(description) > 0:
94 iface["description"] = description
95
David K. Bainbridge5b392b82016-11-15 12:31:14 -080096 if words[0] in iface and words[0] in allow_lists:
97 have = iface[words[0]]
98 if type(have) is list:
99 iface[words[0]].append(" ".join(words[1:]))
100 else:
101 iface[words[0]] = [have, " ".join(words[1:])]
102 else:
103 iface[words[0]] = " ".join(words[1:])
104
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700105 data[current] = iface
106 return current
107
108#####
109# Writers
110#
111# Writers take the form of 'write_<keyword>` where keyword is an interface
112# attribute. The role of the writer is to output the attribute to the
113# output stream, i.e. the new interface file.
114#####
115
116# Writes a generic name / value pair indented
117def write_attr(out, name, value):
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800118 if isinstance(value, list):
119 for line in value:
120 out.write(" %s %s\n" % (name, line))
121 else:
122 out.write(" %s %s\n" % (name, value))
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700123
124# Writes an interface definition to the output stream
125def write_iface(out, name, iface):
126 if "description" in iface.keys():
127 val = iface["description"]
128 if len(val) > 0 and val[0] != "#":
129 val = "# " + val
130 out.write("%s\n" % (val))
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800131 if "auto" in iface.keys() and iface["auto"]:
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700132 out.write("auto %s\n" % (name))
133 out.write("iface %s %s %s\n" % (name, iface["type"], iface["config"]))
134 for attr in sorted(iface.keys(), key=lambda x:x in write_sort_order.keys() and write_sort_order[x] or 100):
135 if attr in write_ignore:
136 continue
137 writer = "write_%s" % (attr)
138 if writer in all_methods:
139 globals()[writer](out, attr, iface[attr])
140 else:
141 write_attr(out, attr, iface[attr])
142 out.write("\n")
143
144# Writes the new interface file
145def write(out, data):
146# out.write("# This file describes the network interfaces available on your system\n")
147# out.write("# and how to activate them. For more information, see interfaces(5).\n\n")
148 # First to loopback
149 for name, iface in data.items():
150 if iface["config"] != "loopback":
151 continue
152 write_iface(out, name, iface)
153
154 for iface in sorted(data.keys(), key=lambda x:x in write_iface_sort_order.keys() and write_iface_sort_order[x] or x):
155 if data[iface]["config"] == "loopback":
156 continue
157 write_iface(out, iface, data[iface])
158
159# The defaults for the netfile task
160src_file = "/etc/network/interfaces"
161dest_file = None
162merge_comments = False
163state = "present"
164name = ""
165force = False
166values = {
167 "config": "manual",
168 "type": "inet"
169}
170
171# read the argument string from the arguments file
172args_file = sys.argv[1]
173args_data = file(args_file).read()
174
175# parse the task options
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800176arguments = json.loads(args_data)
177for key, value in arguments.iteritems():
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700178 # if setting the time, the key 'time'
179 # will contain the value we want to set the time to
180
181 # Strip off quotes that ansible sometimes adds
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800182 #value = value.strip("\"\' ")
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700183
184 if key == "src":
185 src_file = value
186 elif key == "dest":
187 dest_file = value
188 elif key == "name":
189 name = value
190 elif key == "state":
191 state = value
192 elif key == "force":
193 force = value.lower() in ['true', 't', 'yes', 'y']
194 elif key == "description":
195 values["description"] = value
196 elif key == "merge-comments":
197 merge_comments = value.lower() in ['true', 't', 'yes', 'y']
198 elif key == "address":
199 if string.find(value, "/") != -1:
200 parts = value.split('/')
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800201 addr = ipaddress.ip_network(value, strict=False)
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700202 values["address"] = parts[0]
203 values["network"] = addr.network_address.exploded.encode('ascii','ignore')
204 values["netmask"] = addr.netmask.exploded.encode('ascii','ignore')
205 values["broadcast"] = addr.broadcast_address.exploded.encode('ascii','ignore')
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700206 else:
207 values["address"] = value
208 elif key[0] != '_':
209 values[key] = value
210
211# If no destination file was specified, write it back to the same file
212if not dest_file:
213 dest_file = src_file
214
215# all methods is used to check if parser or writer methods exist
216all_methods = dir()
217
218# which attributes should be ignored and not be written as single
219# attributes values against and interface
220write_ignore = ["auto", "type", "config", "description"]
221
222# specifies the order in which attributes are written against an
223# interface. Any attribute note in this list is sorted by default
224# order after the attributes specified.
225write_sort_order = {
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800226 "address" : 1,
227 "network" : 2,
228 "netmask" : 3,
229 "broadcast" : 4,
230 "gateway" : 5,
231 "pre-up" : 10,
232 "post-up" : 11,
233 "pre-down" : 12,
234 "post-down" : 13
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700235}
236
237write_iface_sort_order = {
238 "fabric" : "y",
239 "mgmtbr" : "z"
240}
241
242# Read and parse the specified interface file
243file = open(src_file, "r")
244ifaces = {}
245current = "" # The current interface being parsed
246description = ""
247for line in file.readlines():
248 line = line.rstrip('\n')
249
250 if comment.match(line):
251 if len(description) > 0:
252 description = description + '\n' + line
253 else:
254 description = line
255
256 if len(description) > 0 and blank.match(line):
257 description = description + '\n'
258
259 # Drop any comment of blank line
260 if comment.match(line) or blank.match(line):
261 continue
262
263 # Parse the line
264 words = line.split()
265 parser = "parse_" + words[0].replace("-", "_")
266 if parser in all_methods:
267 current = globals()[parser](ifaces, current, words, description)
268 else:
269 current = parse_add_attr(ifaces, current, words, description)
270
271 description = ""
272
273file.close()
274
275# Assume no change unless we discover otherwise
276result = {
277 "changed" : False
278}
279change_type = 0
280
281# if the interface specified and state is present then either add
282# it to the model or replace it if it already exists.
283if state == "query":
284 if name in ifaces.keys():
285 result["interface"] = ifaces[name]
286 result["found"] = True
287 else:
288 result["found"] = False
289elif state == "present":
290 if name in ifaces.keys():
291 have = ifaces[name]
292 change_type = compare(have, values)
293 result["change_type"] = change_type
294 if change_type != 0:
295 ifaces[name] = values
296 result["desc"] = ifaces[name]["description"]
297 if merge_comments and "description" in have.keys() and len(have["description"]) > 0:
298 result["merge_comments"] = True
299 if "description" in values.keys() and len(values["description"]) > 0:
300 ifaces[name]["description"] = values["description"] + "\n" + have["description"]
301 else:
302 ifaces[name]["description"] = have["description"]
303 result["changed"] = (change_type == 1)
304 else:
305 ifaces[name] = values
306 result["changed"] = True
307
308
309# if state is absent then remove it from the model
310elif state == "absent" and name in ifaces.keys():
311 del ifaces[name]
312 result["changed"] = True
313
314# Only write the output file if something has changed or if the
315# task requests a forced write.
316if force or result["changed"] or change_type != 0:
317 file = open(dest_file, "w+")
318 write(file, ifaces)
319 file.close()
320
321# Output the task result
322print json.dumps(result)