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