blob: 98463df82cb52d99d06931e175e894e247a07743 [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')
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700193 else:
194 values["address"] = value
195 elif key[0] != '_':
196 values[key] = value
197
198# If no destination file was specified, write it back to the same file
199if not dest_file:
200 dest_file = src_file
201
202# all methods is used to check if parser or writer methods exist
203all_methods = dir()
204
205# which attributes should be ignored and not be written as single
206# attributes values against and interface
207write_ignore = ["auto", "type", "config", "description"]
208
209# specifies the order in which attributes are written against an
210# interface. Any attribute note in this list is sorted by default
211# order after the attributes specified.
212write_sort_order = {
213 "address" : 1,
214 "network" : 2,
215 "netmask" : 3,
216 "broadcast" : 4,
217 "gateway" : 5
218}
219
220write_iface_sort_order = {
221 "fabric" : "y",
222 "mgmtbr" : "z"
223}
224
225# Read and parse the specified interface file
226file = open(src_file, "r")
227ifaces = {}
228current = "" # The current interface being parsed
229description = ""
230for line in file.readlines():
231 line = line.rstrip('\n')
232
233 if comment.match(line):
234 if len(description) > 0:
235 description = description + '\n' + line
236 else:
237 description = line
238
239 if len(description) > 0 and blank.match(line):
240 description = description + '\n'
241
242 # Drop any comment of blank line
243 if comment.match(line) or blank.match(line):
244 continue
245
246 # Parse the line
247 words = line.split()
248 parser = "parse_" + words[0].replace("-", "_")
249 if parser in all_methods:
250 current = globals()[parser](ifaces, current, words, description)
251 else:
252 current = parse_add_attr(ifaces, current, words, description)
253
254 description = ""
255
256file.close()
257
258# Assume no change unless we discover otherwise
259result = {
260 "changed" : False
261}
262change_type = 0
263
264# if the interface specified and state is present then either add
265# it to the model or replace it if it already exists.
266if state == "query":
267 if name in ifaces.keys():
268 result["interface"] = ifaces[name]
269 result["found"] = True
270 else:
271 result["found"] = False
272elif state == "present":
273 if name in ifaces.keys():
274 have = ifaces[name]
275 change_type = compare(have, values)
276 result["change_type"] = change_type
277 if change_type != 0:
278 ifaces[name] = values
279 result["desc"] = ifaces[name]["description"]
280 if merge_comments and "description" in have.keys() and len(have["description"]) > 0:
281 result["merge_comments"] = True
282 if "description" in values.keys() and len(values["description"]) > 0:
283 ifaces[name]["description"] = values["description"] + "\n" + have["description"]
284 else:
285 ifaces[name]["description"] = have["description"]
286 result["changed"] = (change_type == 1)
287 else:
288 ifaces[name] = values
289 result["changed"] = True
290
291
292# if state is absent then remove it from the model
293elif state == "absent" and name in ifaces.keys():
294 del ifaces[name]
295 result["changed"] = True
296
297# Only write the output file if something has changed or if the
298# task requests a forced write.
299if force or result["changed"] or change_type != 0:
300 file = open(dest_file, "w+")
301 write(file, ifaces)
302 file.close()
303
304# Output the task result
305print json.dumps(result)