blob: ce0b47429492eb038ec81edf6bc8dc0db50f2147 [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.
David K. Bainbridgef757bc72016-11-16 21:45:52 -080032def value_equal(left, right):
33 if type(left) == type(right):
34 return left == right
35 return str(left) == str(right)
36
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070037def compare(have, want):
38 result = 0
39 for key in list(set().union(have.keys(), want.keys())):
40 if key in have.keys() and key in want.keys():
David K. Bainbridgef757bc72016-11-16 21:45:52 -080041 if not value_equal(have[key], want[key]):
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070042 if key in ["description"]:
43 result = -1
44 else:
45 return 1
46 else:
47 if key in ["description"]:
48 result = -1
49 else:
50 return 1
51 return result
52
53# Creates an interface definition in the model and sets the auto
54# configuration to true
55def parse_auto(data, current, words, description):
56 if words[1] in data.keys():
57 iface = data[words[1]]
58 else:
59 iface = {}
60
61 if len(description) > 0:
62 iface["description"] = description
63
David K. Bainbridge5b392b82016-11-15 12:31:14 -080064 iface["auto"] = True
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070065 data[words[1]] = iface
66 return words[1]
67
68# Creates an interface definition in the model if one does not exist and
69# sets the type and configuation method
70def parse_iface(data, current, words, description):
71 if words[1] in data.keys():
72 iface = data[words[1]]
73 else:
74 iface = {}
75
76 if len(description) > 0:
77 iface["description"] = description
78
79 iface["type"] = words[2]
80 iface["config"] = words[3]
81 data[words[1]] = iface
82 return words[1]
83
David K. Bainbridge5b392b82016-11-15 12:31:14 -080084allow_lists = ["pre-up", "post-up", "pre-down", "post-down"]
85
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070086# Used to evaluate attributes and add a generic name / value pair to the interface
87# model
88def parse_add_attr(data, current, words, description):
David K. Bainbridge5b392b82016-11-15 12:31:14 -080089 global allow_lists
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070090 if current == "":
91 raise SyntaxError("Attempt to add attribute '%s' without an interface" % words[0])
92
93 if current in data.keys():
94 iface = data[current]
95 else:
96 iface = {}
97
98 if len(description) > 0:
99 iface["description"] = description
100
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800101 if words[0] in iface and words[0] in allow_lists:
102 have = iface[words[0]]
103 if type(have) is list:
104 iface[words[0]].append(" ".join(words[1:]))
105 else:
106 iface[words[0]] = [have, " ".join(words[1:])]
107 else:
108 iface[words[0]] = " ".join(words[1:])
109
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700110 data[current] = iface
111 return current
112
113#####
114# Writers
115#
116# Writers take the form of 'write_<keyword>` where keyword is an interface
117# attribute. The role of the writer is to output the attribute to the
118# output stream, i.e. the new interface file.
119#####
120
121# Writes a generic name / value pair indented
122def write_attr(out, name, value):
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800123 if isinstance(value, list):
124 for line in value:
125 out.write(" %s %s\n" % (name, line))
126 else:
127 out.write(" %s %s\n" % (name, value))
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700128
129# Writes an interface definition to the output stream
130def write_iface(out, name, iface):
131 if "description" in iface.keys():
132 val = iface["description"]
133 if len(val) > 0 and val[0] != "#":
134 val = "# " + val
135 out.write("%s\n" % (val))
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800136 if "auto" in iface.keys() and iface["auto"]:
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700137 out.write("auto %s\n" % (name))
138 out.write("iface %s %s %s\n" % (name, iface["type"], iface["config"]))
139 for attr in sorted(iface.keys(), key=lambda x:x in write_sort_order.keys() and write_sort_order[x] or 100):
140 if attr in write_ignore:
141 continue
142 writer = "write_%s" % (attr)
143 if writer in all_methods:
144 globals()[writer](out, attr, iface[attr])
145 else:
146 write_attr(out, attr, iface[attr])
147 out.write("\n")
148
149# Writes the new interface file
150def write(out, data):
151# out.write("# This file describes the network interfaces available on your system\n")
152# out.write("# and how to activate them. For more information, see interfaces(5).\n\n")
153 # First to loopback
154 for name, iface in data.items():
155 if iface["config"] != "loopback":
156 continue
157 write_iface(out, name, iface)
158
159 for iface in sorted(data.keys(), key=lambda x:x in write_iface_sort_order.keys() and write_iface_sort_order[x] or x):
160 if data[iface]["config"] == "loopback":
161 continue
162 write_iface(out, iface, data[iface])
163
164# The defaults for the netfile task
165src_file = "/etc/network/interfaces"
166dest_file = None
167merge_comments = False
168state = "present"
169name = ""
170force = False
171values = {
172 "config": "manual",
173 "type": "inet"
174}
175
176# read the argument string from the arguments file
177args_file = sys.argv[1]
178args_data = file(args_file).read()
179
180# parse the task options
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800181arguments = json.loads(args_data)
182for key, value in arguments.iteritems():
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700183 if key == "src":
184 src_file = value
185 elif key == "dest":
186 dest_file = value
187 elif key == "name":
188 name = value
189 elif key == "state":
190 state = value
191 elif key == "force":
192 force = value.lower() in ['true', 't', 'yes', 'y']
193 elif key == "description":
194 values["description"] = value
195 elif key == "merge-comments":
196 merge_comments = value.lower() in ['true', 't', 'yes', 'y']
197 elif key == "address":
198 if string.find(value, "/") != -1:
199 parts = value.split('/')
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800200 addr = ipaddress.ip_network(value, strict=False)
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700201 values["address"] = parts[0]
202 values["network"] = addr.network_address.exploded.encode('ascii','ignore')
203 values["netmask"] = addr.netmask.exploded.encode('ascii','ignore')
204 values["broadcast"] = addr.broadcast_address.exploded.encode('ascii','ignore')
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700205 else:
206 values["address"] = value
207 elif key[0] != '_':
208 values[key] = value
209
David K. Bainbridge175aa9d2016-11-17 10:32:44 -0800210# If name is not set we need to error out
211if name == "":
212 result = {
213 "changed": False,
David K. Bainbridge6e23ac82016-12-07 12:55:41 -0800214 "failed": True,
David K. Bainbridge175aa9d2016-11-17 10:32:44 -0800215 "msg": "Name is a mansitory parameter",
216 }
217 print json.dumps(result)
218 sys.stdout.flush()
219 exit(1)
220
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700221# If no destination file was specified, write it back to the same file
222if not dest_file:
223 dest_file = src_file
224
225# all methods is used to check if parser or writer methods exist
226all_methods = dir()
227
228# which attributes should be ignored and not be written as single
229# attributes values against and interface
230write_ignore = ["auto", "type", "config", "description"]
231
232# specifies the order in which attributes are written against an
233# interface. Any attribute note in this list is sorted by default
234# order after the attributes specified.
235write_sort_order = {
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800236 "address" : 1,
237 "network" : 2,
238 "netmask" : 3,
239 "broadcast" : 4,
240 "gateway" : 5,
241 "pre-up" : 10,
242 "post-up" : 11,
243 "pre-down" : 12,
244 "post-down" : 13
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700245}
246
247write_iface_sort_order = {
248 "fabric" : "y",
249 "mgmtbr" : "z"
250}
251
252# Read and parse the specified interface file
253file = open(src_file, "r")
254ifaces = {}
255current = "" # The current interface being parsed
256description = ""
257for line in file.readlines():
258 line = line.rstrip('\n')
259
260 if comment.match(line):
261 if len(description) > 0:
262 description = description + '\n' + line
263 else:
264 description = line
265
266 if len(description) > 0 and blank.match(line):
267 description = description + '\n'
268
269 # Drop any comment of blank line
270 if comment.match(line) or blank.match(line):
271 continue
272
273 # Parse the line
274 words = line.split()
275 parser = "parse_" + words[0].replace("-", "_")
276 if parser in all_methods:
277 current = globals()[parser](ifaces, current, words, description)
278 else:
279 current = parse_add_attr(ifaces, current, words, description)
280
281 description = ""
282
283file.close()
284
285# Assume no change unless we discover otherwise
286result = {
287 "changed" : False
288}
289change_type = 0
290
291# if the interface specified and state is present then either add
292# it to the model or replace it if it already exists.
293if state == "query":
294 if name in ifaces.keys():
295 result["interface"] = ifaces[name]
296 result["found"] = True
297 else:
298 result["found"] = False
299elif state == "present":
300 if name in ifaces.keys():
301 have = ifaces[name]
302 change_type = compare(have, values)
303 result["change_type"] = change_type
304 if change_type != 0:
305 ifaces[name] = values
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700306 if merge_comments and "description" in have.keys() and len(have["description"]) > 0:
307 result["merge_comments"] = True
308 if "description" in values.keys() and len(values["description"]) > 0:
309 ifaces[name]["description"] = values["description"] + "\n" + have["description"]
310 else:
311 ifaces[name]["description"] = have["description"]
312 result["changed"] = (change_type == 1)
313 else:
314 ifaces[name] = values
315 result["changed"] = True
316
317
318# if state is absent then remove it from the model
319elif state == "absent" and name in ifaces.keys():
320 del ifaces[name]
321 result["changed"] = True
322
323# Only write the output file if something has changed or if the
324# task requests a forced write.
325if force or result["changed"] or change_type != 0:
326 file = open(dest_file, "w+")
327 write(file, ifaces)
328 file.close()
329
330# Output the task result
331print json.dumps(result)