blob: 86868bfdd2cbe96d5569b104a6d3ec48d46b946a [file] [log] [blame]
#!/usr/bin/env python
import json
import os
import re
import sys
import shlex
import string
import ipaddress
# WANT_JSON
# Regular expressions to identify comments and blank lines
comment = re.compile("^\s*#")
blank = re.compile("^\s*$")
#####
# Parsers
#
# Parses are methods that take the form 'parse_<keyword>', where the keyword
# is the first word on a line in file. The purpose of the parser is to
# evaluate the line tna update the interface model accordingly.
####
# Compares the current and desired network configuration to see if there
# is a change and returns:
# 0 if no change
# -1 if the change has no semantic value (i.e. comments differ)
# 1 if there is a semantic change (i.e. its meaningful)
# the highest priority of change is returned, i.e. if there is both
# a semantic and non-semantic change a 1 is returned indicating a
# semantic change.
def value_equal(left, right):
if type(left) == type(right):
return left == right
return str(left) == str(right)
def compare(have, want):
result = 0
for key in list(set().union(have.keys(), want.keys())):
if key in have.keys() and key in want.keys():
if not value_equal(have[key], want[key]):
if key in ["description"]:
result = -1
else:
return 1
else:
if key in ["description"]:
result = -1
else:
return 1
return result
# Creates an interface definition in the model and sets the auto
# configuration to true
def parse_auto(data, current, words, description):
if words[1] in data.keys():
iface = data[words[1]]
else:
iface = {}
if len(description) > 0:
iface["description"] = description
iface["auto"] = True
data[words[1]] = iface
return words[1]
# Creates an interface definition in the model if one does not exist and
# sets the type and configuation method
def parse_iface(data, current, words, description):
if words[1] in data.keys():
iface = data[words[1]]
else:
iface = {}
if len(description) > 0:
iface["description"] = description
iface["type"] = words[2]
iface["config"] = words[3]
data[words[1]] = iface
return words[1]
allow_lists = ["pre-up", "post-up", "pre-down", "post-down"]
# Used to evaluate attributes and add a generic name / value pair to the interface
# model
def parse_add_attr(data, current, words, description):
global allow_lists
if current == "":
raise SyntaxError("Attempt to add attribute '%s' without an interface" % words[0])
if current in data.keys():
iface = data[current]
else:
iface = {}
if len(description) > 0:
iface["description"] = description
if words[0] in iface and words[0] in allow_lists:
have = iface[words[0]]
if type(have) is list:
iface[words[0]].append(" ".join(words[1:]))
else:
iface[words[0]] = [have, " ".join(words[1:])]
else:
iface[words[0]] = " ".join(words[1:])
data[current] = iface
return current
#####
# Writers
#
# Writers take the form of 'write_<keyword>` where keyword is an interface
# attribute. The role of the writer is to output the attribute to the
# output stream, i.e. the new interface file.
#####
# Writes a generic name / value pair indented
def write_attr(out, name, value):
if isinstance(value, list):
for line in value:
out.write(" %s %s\n" % (name, line))
else:
out.write(" %s %s\n" % (name, value))
# Writes an interface definition to the output stream
def write_iface(out, name, iface):
if "description" in iface.keys():
val = iface["description"]
if len(val) > 0 and val[0] != "#":
val = "# " + val
out.write("%s\n" % (val))
if "auto" in iface.keys() and iface["auto"]:
out.write("auto %s\n" % (name))
out.write("iface %s %s %s\n" % (name, iface["type"], iface["config"]))
for attr in sorted(iface.keys(), key=lambda x:x in write_sort_order.keys() and write_sort_order[x] or 100):
if attr in write_ignore:
continue
writer = "write_%s" % (attr)
if writer in all_methods:
globals()[writer](out, attr, iface[attr])
else:
write_attr(out, attr, iface[attr])
out.write("\n")
# Writes the new interface file
def write(out, data):
# out.write("# This file describes the network interfaces available on your system\n")
# out.write("# and how to activate them. For more information, see interfaces(5).\n\n")
# First to loopback
for name, iface in data.items():
if iface["config"] != "loopback":
continue
write_iface(out, name, iface)
for iface in sorted(data.keys(), key=lambda x:x in write_iface_sort_order.keys() and write_iface_sort_order[x] or x):
if data[iface]["config"] == "loopback":
continue
write_iface(out, iface, data[iface])
# The defaults for the netfile task
src_file = "/etc/network/interfaces"
dest_file = None
merge_comments = False
state = "present"
name = ""
force = False
values = {
"config": "manual",
"type": "inet"
}
# read the argument string from the arguments file
args_file = sys.argv[1]
args_data = file(args_file).read()
# parse the task options
arguments = json.loads(args_data)
for key, value in arguments.iteritems():
if key == "src":
src_file = value
elif key == "dest":
dest_file = value
elif key == "name":
name = value
elif key == "state":
state = value
elif key == "force":
force = value.lower() in ['true', 't', 'yes', 'y']
elif key == "description":
values["description"] = value
elif key == "merge-comments":
merge_comments = value.lower() in ['true', 't', 'yes', 'y']
elif key == "address":
if string.find(value, "/") != -1:
parts = value.split('/')
addr = ipaddress.ip_network(value, strict=False)
values["address"] = parts[0]
values["network"] = addr.network_address.exploded.encode('ascii','ignore')
values["netmask"] = addr.netmask.exploded.encode('ascii','ignore')
values["broadcast"] = addr.broadcast_address.exploded.encode('ascii','ignore')
else:
values["address"] = value
elif key[0] != '_':
values[key] = value
# If name is not set we need to error out
if name == "":
result = {
"changed": False,
"failed": True,
"msg": "Name is a mansitory parameter",
}
print json.dumps(result)
sys.stdout.flush()
exit(1)
# If no destination file was specified, write it back to the same file
if not dest_file:
dest_file = src_file
# all methods is used to check if parser or writer methods exist
all_methods = dir()
# which attributes should be ignored and not be written as single
# attributes values against and interface
write_ignore = ["auto", "type", "config", "description"]
# specifies the order in which attributes are written against an
# interface. Any attribute note in this list is sorted by default
# order after the attributes specified.
write_sort_order = {
"address" : 1,
"network" : 2,
"netmask" : 3,
"broadcast" : 4,
"gateway" : 5,
"pre-up" : 10,
"post-up" : 11,
"pre-down" : 12,
"post-down" : 13
}
write_iface_sort_order = {
"fabric" : "y",
"mgmtbr" : "z"
}
# Read and parse the specified interface file
file = open(src_file, "r")
ifaces = {}
current = "" # The current interface being parsed
description = ""
for line in file.readlines():
line = line.rstrip('\n')
if comment.match(line):
if len(description) > 0:
description = description + '\n' + line
else:
description = line
if len(description) > 0 and blank.match(line):
description = description + '\n'
# Drop any comment of blank line
if comment.match(line) or blank.match(line):
continue
# Parse the line
words = line.split()
parser = "parse_" + words[0].replace("-", "_")
if parser in all_methods:
current = globals()[parser](ifaces, current, words, description)
else:
current = parse_add_attr(ifaces, current, words, description)
description = ""
file.close()
# Assume no change unless we discover otherwise
result = {
"changed" : False
}
change_type = 0
# if the interface specified and state is present then either add
# it to the model or replace it if it already exists.
if state == "query":
if name in ifaces.keys():
result["interface"] = ifaces[name]
result["found"] = True
else:
result["found"] = False
elif state == "present":
if name in ifaces.keys():
have = ifaces[name]
change_type = compare(have, values)
result["change_type"] = change_type
if change_type != 0:
ifaces[name] = values
if merge_comments and "description" in have.keys() and len(have["description"]) > 0:
result["merge_comments"] = True
if "description" in values.keys() and len(values["description"]) > 0:
ifaces[name]["description"] = values["description"] + "\n" + have["description"]
else:
ifaces[name]["description"] = have["description"]
result["changed"] = (change_type == 1)
else:
ifaces[name] = values
result["changed"] = True
# if state is absent then remove it from the model
elif state == "absent" and name in ifaces.keys():
del ifaces[name]
result["changed"] = True
# Only write the output file if something has changed or if the
# task requests a forced write.
if force or result["changed"] or change_type != 0:
file = open(dest_file, "w+")
write(file, ifaces)
file.close()
# Output the task result
print json.dumps(result)