blob: bb37f43b21681a16bb64ac27238eb22c7cb43678 [file] [log] [blame]
David K. Bainbridgea677d4e2016-09-11 20:01:32 -07001#!/usr/bin/env python
2
Jonathan Hart93956f52017-08-22 13:12:42 -07003# Copyright 2017-present Open Networking Foundation
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070017import json
18import os
19import re
20import sys
21import shlex
22import string
23import ipaddress
David K. Bainbridge5b392b82016-11-15 12:31:14 -080024# WANT_JSON
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070025
26# Regular expressions to identify comments and blank lines
27comment = re.compile("^\s*#")
28blank = re.compile("^\s*$")
29
30#####
31# Parsers
32#
33# Parses are methods that take the form 'parse_<keyword>', where the keyword
34# is the first word on a line in file. The purpose of the parser is to
35# evaluate the line tna update the interface model accordingly.
36####
37
38# Compares the current and desired network configuration to see if there
39# is a change and returns:
40# 0 if no change
41# -1 if the change has no semantic value (i.e. comments differ)
42# 1 if there is a semantic change (i.e. its meaningful)
43# the highest priority of change is returned, i.e. if there is both
44# a semantic and non-semantic change a 1 is returned indicating a
45# semantic change.
David K. Bainbridgef757bc72016-11-16 21:45:52 -080046def value_equal(left, right):
47 if type(left) == type(right):
48 return left == right
49 return str(left) == str(right)
50
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070051def compare(have, want):
52 result = 0
53 for key in list(set().union(have.keys(), want.keys())):
54 if key in have.keys() and key in want.keys():
David K. Bainbridgef757bc72016-11-16 21:45:52 -080055 if not value_equal(have[key], want[key]):
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070056 if key in ["description"]:
57 result = -1
58 else:
59 return 1
60 else:
61 if key in ["description"]:
62 result = -1
63 else:
64 return 1
65 return result
66
67# Creates an interface definition in the model and sets the auto
68# configuration to true
69def parse_auto(data, current, words, description):
70 if words[1] in data.keys():
71 iface = data[words[1]]
72 else:
73 iface = {}
74
75 if len(description) > 0:
76 iface["description"] = description
77
David K. Bainbridge5b392b82016-11-15 12:31:14 -080078 iface["auto"] = True
David K. Bainbridgea677d4e2016-09-11 20:01:32 -070079 data[words[1]] = iface
80 return words[1]
81
82# Creates an interface definition in the model if one does not exist and
83# sets the type and configuation method
84def parse_iface(data, current, words, description):
85 if words[1] in data.keys():
86 iface = data[words[1]]
87 else:
88 iface = {}
89
90 if len(description) > 0:
91 iface["description"] = description
92
93 iface["type"] = words[2]
94 iface["config"] = words[3]
95 data[words[1]] = iface
96 return words[1]
97
David K. Bainbridge5b392b82016-11-15 12:31:14 -080098allow_lists = ["pre-up", "post-up", "pre-down", "post-down"]
99
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700100# Used to evaluate attributes and add a generic name / value pair to the interface
101# model
102def parse_add_attr(data, current, words, description):
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800103 global allow_lists
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700104 if current == "":
105 raise SyntaxError("Attempt to add attribute '%s' without an interface" % words[0])
106
107 if current in data.keys():
108 iface = data[current]
109 else:
110 iface = {}
111
112 if len(description) > 0:
113 iface["description"] = description
114
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800115 if words[0] in iface and words[0] in allow_lists:
116 have = iface[words[0]]
117 if type(have) is list:
118 iface[words[0]].append(" ".join(words[1:]))
119 else:
120 iface[words[0]] = [have, " ".join(words[1:])]
121 else:
122 iface[words[0]] = " ".join(words[1:])
123
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700124 data[current] = iface
125 return current
126
127#####
128# Writers
129#
130# Writers take the form of 'write_<keyword>` where keyword is an interface
131# attribute. The role of the writer is to output the attribute to the
132# output stream, i.e. the new interface file.
133#####
134
135# Writes a generic name / value pair indented
136def write_attr(out, name, value):
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800137 if isinstance(value, list):
138 for line in value:
139 out.write(" %s %s\n" % (name, line))
140 else:
141 out.write(" %s %s\n" % (name, value))
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700142
143# Writes an interface definition to the output stream
144def write_iface(out, name, iface):
145 if "description" in iface.keys():
146 val = iface["description"]
147 if len(val) > 0 and val[0] != "#":
148 val = "# " + val
149 out.write("%s\n" % (val))
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800150 if "auto" in iface.keys() and iface["auto"]:
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700151 out.write("auto %s\n" % (name))
152 out.write("iface %s %s %s\n" % (name, iface["type"], iface["config"]))
153 for attr in sorted(iface.keys(), key=lambda x:x in write_sort_order.keys() and write_sort_order[x] or 100):
154 if attr in write_ignore:
155 continue
156 writer = "write_%s" % (attr)
157 if writer in all_methods:
158 globals()[writer](out, attr, iface[attr])
159 else:
160 write_attr(out, attr, iface[attr])
161 out.write("\n")
162
163# Writes the new interface file
164def write(out, data):
165# out.write("# This file describes the network interfaces available on your system\n")
166# out.write("# and how to activate them. For more information, see interfaces(5).\n\n")
167 # First to loopback
168 for name, iface in data.items():
169 if iface["config"] != "loopback":
170 continue
171 write_iface(out, name, iface)
172
173 for iface in sorted(data.keys(), key=lambda x:x in write_iface_sort_order.keys() and write_iface_sort_order[x] or x):
174 if data[iface]["config"] == "loopback":
175 continue
176 write_iface(out, iface, data[iface])
177
178# The defaults for the netfile task
179src_file = "/etc/network/interfaces"
180dest_file = None
181merge_comments = False
182state = "present"
183name = ""
184force = False
185values = {
186 "config": "manual",
187 "type": "inet"
188}
189
190# read the argument string from the arguments file
191args_file = sys.argv[1]
192args_data = file(args_file).read()
193
194# parse the task options
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800195arguments = json.loads(args_data)
196for key, value in arguments.iteritems():
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700197 if key == "src":
198 src_file = value
199 elif key == "dest":
200 dest_file = value
201 elif key == "name":
202 name = value
203 elif key == "state":
204 state = value
205 elif key == "force":
206 force = value.lower() in ['true', 't', 'yes', 'y']
207 elif key == "description":
208 values["description"] = value
209 elif key == "merge-comments":
210 merge_comments = value.lower() in ['true', 't', 'yes', 'y']
211 elif key == "address":
212 if string.find(value, "/") != -1:
213 parts = value.split('/')
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800214 addr = ipaddress.ip_network(value, strict=False)
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700215 values["address"] = parts[0]
216 values["network"] = addr.network_address.exploded.encode('ascii','ignore')
217 values["netmask"] = addr.netmask.exploded.encode('ascii','ignore')
218 values["broadcast"] = addr.broadcast_address.exploded.encode('ascii','ignore')
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700219 else:
220 values["address"] = value
221 elif key[0] != '_':
222 values[key] = value
223
David K. Bainbridge175aa9d2016-11-17 10:32:44 -0800224# If name is not set we need to error out
225if name == "":
226 result = {
227 "changed": False,
David K. Bainbridge6e23ac82016-12-07 12:55:41 -0800228 "failed": True,
David K. Bainbridge175aa9d2016-11-17 10:32:44 -0800229 "msg": "Name is a mansitory parameter",
230 }
231 print json.dumps(result)
232 sys.stdout.flush()
233 exit(1)
234
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700235# If no destination file was specified, write it back to the same file
236if not dest_file:
237 dest_file = src_file
238
239# all methods is used to check if parser or writer methods exist
240all_methods = dir()
241
242# which attributes should be ignored and not be written as single
243# attributes values against and interface
244write_ignore = ["auto", "type", "config", "description"]
245
246# specifies the order in which attributes are written against an
247# interface. Any attribute note in this list is sorted by default
248# order after the attributes specified.
249write_sort_order = {
David K. Bainbridge5b392b82016-11-15 12:31:14 -0800250 "address" : 1,
251 "network" : 2,
252 "netmask" : 3,
253 "broadcast" : 4,
254 "gateway" : 5,
255 "pre-up" : 10,
256 "post-up" : 11,
257 "pre-down" : 12,
258 "post-down" : 13
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700259}
260
261write_iface_sort_order = {
262 "fabric" : "y",
263 "mgmtbr" : "z"
264}
265
266# Read and parse the specified interface file
267file = open(src_file, "r")
268ifaces = {}
269current = "" # The current interface being parsed
270description = ""
271for line in file.readlines():
272 line = line.rstrip('\n')
273
274 if comment.match(line):
275 if len(description) > 0:
276 description = description + '\n' + line
277 else:
278 description = line
279
280 if len(description) > 0 and blank.match(line):
281 description = description + '\n'
282
283 # Drop any comment of blank line
284 if comment.match(line) or blank.match(line):
285 continue
286
287 # Parse the line
288 words = line.split()
289 parser = "parse_" + words[0].replace("-", "_")
290 if parser in all_methods:
291 current = globals()[parser](ifaces, current, words, description)
292 else:
293 current = parse_add_attr(ifaces, current, words, description)
294
295 description = ""
296
297file.close()
298
299# Assume no change unless we discover otherwise
300result = {
301 "changed" : False
302}
303change_type = 0
304
305# if the interface specified and state is present then either add
306# it to the model or replace it if it already exists.
307if state == "query":
308 if name in ifaces.keys():
309 result["interface"] = ifaces[name]
310 result["found"] = True
311 else:
312 result["found"] = False
313elif state == "present":
314 if name in ifaces.keys():
315 have = ifaces[name]
316 change_type = compare(have, values)
317 result["change_type"] = change_type
318 if change_type != 0:
319 ifaces[name] = values
David K. Bainbridgea677d4e2016-09-11 20:01:32 -0700320 if merge_comments and "description" in have.keys() and len(have["description"]) > 0:
321 result["merge_comments"] = True
322 if "description" in values.keys() and len(values["description"]) > 0:
323 ifaces[name]["description"] = values["description"] + "\n" + have["description"]
324 else:
325 ifaces[name]["description"] = have["description"]
326 result["changed"] = (change_type == 1)
327 else:
328 ifaces[name] = values
329 result["changed"] = True
330
331
332# if state is absent then remove it from the model
333elif state == "absent" and name in ifaces.keys():
334 del ifaces[name]
335 result["changed"] = True
336
337# Only write the output file if something has changed or if the
338# task requests a forced write.
339if force or result["changed"] or change_type != 0:
340 file = open(dest_file, "w+")
341 write(file, ifaces)
342 file.close()
343
344# Output the task result
345print json.dumps(result)