#!/usr/bin/python3
import os
import sys
import string
from optparse import OptionParser
from optparse import OptionGroup


gcfgpath = "/etc/sysconfig/network-scripts/"

class ConfigFile(object):
	"""
	Base Class for Configuration files.  All used Configuration file Types should
	inherit from this base class, but it should never be used on its own
	"""
	def __init__(self, iface):
		self.iface = iface
		self.basepath = gcfgpath

	def save(self):
		raise TypeError

	def load(self):
		raise TypeError

	def findAllConfiguredInterfaces(self):
		raise NotImplementedError()

class IfcfgConfigfFile(ConfigFile):
	"""
	Legacy Interface configuration file.  Normally found in
	/etc/sysconfig/network-scripts/ifcfg-*, these files store text key/value pairs
	and define various network interfaces.  Used by the legacy sysv init scripts
	"""
	def __init__(self, iface):
		global gcfgpath
		super(IfcfgConfigfFile, self).__init__(iface)
		self.path = self.basepath+"ifcfg-"+iface.name

	def save(self):
		try:
			fd = open(self.path, "w+")
		except:
			raise IfaceError("Can't save file")
		for i in self.iface.config.keys():
			fd.write(i+"="+"\""+self.iface.config[i]+"\"\n")
		fd.close()

	def load(self):
		try:
			fd = open(self.path, "r+")
		except:
			raise IfaceError("Can't open file")

		for line in fd:
			#skip empty lines
			if (len(line) <= 1):
				continue

			#remove whitespace
			line = line.strip()

			#comment line
			if (line[0] == '#'):
				continue

			entry = line.partition("=");
			self.iface.config[entry[0]] = entry[2].strip("\"")
	def create(self):
		if (os.path.isfile(self.path)):
			raise IfaceError("File Exists")
		self.save()

	def findAllConfiguredInterfaces(self):
		ifnames = []
		devs = []
		# This scans the base path of this config file for
		# All configured interfaces that have this type of config file
		for f in os.listdir(self.basepath):
			if (f[0:6] == "ifcfg-"):
				ifnames.append(f[6:])
		for i in ifnames:
			dev = iface(i)
			try:
				dev.load()
				devs.append(dev)
			except:
				pass

		return devs

class IfaceError(Exception):
	"""
	Error type thrown when an interface operation cannot be completed
	"""
	def __init__(self, errmsg):
		self.errmsg = errmsg


class iface(object):
	"""
	Base Class for an interface.  Its type defaults to ethernet.  It is
	extended such that other types of interfaces (vlans/bonds/bridges/etc),
	can be supported
	"""
	ETH_TYPE = 0
	BOND_TYPE = 1
	VLAN_TYPE = 2
	iftypename = 'eth'

	def __init__(self, ifname):
		self.name = ifname
		self.config = {}
		self.config['DEVICE'] = ifname
		self.config['ICFG_TYPE'] = "ETHERNET"
		self.config['TYPE'] = "Ethernet"
		self.iftype = iface.ETH_TYPE

		# This will become more varied in the future
		# as we start to support netcf or other xml
		#interface definition files
		self.cfgfile = IfcfgConfigfFile(self)
		
	def save(self):
		self.cfgfile.save()

	def load(self):
		self.config = {}
		self.cfgfile.load()

	def create(self):
		self.cfgfile.create()

	def dump(self):
		for i in self.config.keys():
                        print(i,"=","\"",self.config[i],"\"", sep="")

	def removeKey(self, key):
		try:
			del self.config[key]
		except:
			pass

	def updateKey(self, key, value):
		value = value.strip("\"")
		self.config[key]=value
		
	def update(self, newvals):
		for entry in newvals:
			keyval = entry.partition("=")
			if (keyval[1] == ''):
				#This is a removeKey operation
				self.removeKey(keyval[0])
			elif (keyval[1] == '=' and keyval[2]):
				self.updateKey(keyval[0],keyval[2])
			else:
				raise IfaceError(entry + " is an invalid format")

	def getSpecificIfType(self):
		# Here we deteremine exactly what type of interface this is
		# And return a new instance of that type
		newiface = EthIface(self.name)

		# default to 'ETHERNET' if no key exists
		if ('ICFG_TYPE' not in self.config):
			self.config['ICFG_TYPE'] = "ETHERNET"

		if (self.config['ICFG_TYPE'] == "BOND_MASTER"):
			newiface = BondIface(self.name)
			newiface.load_slaves()

		newiface.config = self.config
		newiface.cfgfile = self.cfgfile
		return newiface


class EthIface(iface):
	"""
	Ethernet specific interface subclass
	"""
	iftypename = 'eth'

	def __init__(self, ifname):
		super(EthIface, self).__init__(ifname)

class BondIface(iface):
	"""
	Bonding interface subclass.  Represents the bond interface, not the
	slaves.
	"""
	iftypename = 'bond'

	def __init__(self, ifname):
		super(BondIface, self).__init__(ifname)
		self.iftype = iface.BOND_TYPE
		self.config['ICFG_TYPE'] = "BOND_MASTER" 
		self.slaves = []
		# List of slave to save
		self.saveslaves = []

	def updateKey(self, key, value):
		# Override the superclass here to add bond specific checks
		if (key == 'MASTER'):
			raise IfaceError("Can't enslave a bond device")
		super(BondIface, self).updateKey(key, value)

	def save(self):
		# Save the bonds slaves, if any
		for slave in self.saveslaves:
			slave.save()
		super(BondIface, self).save()

	def load_slaves(self):
		# Scan all the interface configs available and locate the slaves
		# of this bond
		devs = self.cfgfile.findAllConfiguredInterfaces()
		for d in devs:
			try:
				if (d.config['MASTER'] == self.name and d.config['SLAVE'] == 'yes'):
					self.slaves.append(d)
			except:
				pass

		
	def enslave(self, slavename):
		try:
			slave = EthIface(slavename)
			slave.load()
		except IfaceError as e:
			raise IfaceError("No such slave: " + e.errmsg)

		slave.updateKey('MASTER', self.name)
		slave.updateKey('SLAVE', 'yes')
		self.saveslaves.append(slave)
		return

	def deslave(self, slavename):
		try:
			slave = EthIface(slavename)
			slave.load()
		except IfaceError as e:
			raise IfaceError(" No such slave: " + e.errmsg)
		# We don't want to remove the slave if this isn't the bond it
		# belongs to
		try:
			if (slave.config['MASTER'] != self.name):
				raise IfaceError("Slave not owned by bond")
		except KeyError:
			pass

		slave.removeKey('MASTER')
		slave.removeKey('SLAVE')
		self.saveslaves.append(slave)
		return

class VlanIface(iface):
	"""
	Vlan interface subclass.  Represents a Vlan interface
	"""
	iftypename = 'vlan'
	VLAN_TYPE_PDEV = 0
	VLAN_TYPE_NPV = 1

	def __init__(self, ifname):
		super(VlanIface, self).__init__(ifname)
		self.iftype = iface.VLAN_TYPE
		self.config['ICFG_TYPE'] = "VLAN" 
		self.config['VLAN'] = "yes"
		self.vlantype = self.VLAN_TYPE_PDEV

		# Name plus vid type vlans have to have
		# a '.' character in them, and the characters
		# preceding that dot must represent a physical 
		# interface
		dotidx = ifname.find('.')
		if (dotidx == -1):
			self.vlantype = self.VLAN_TYPE_PDEV
			# PDEV type vlans have to specify 
			# Their physical interfaces explicitly
			# as they are not specified by the interface name
			self.config['PHYSDEV'] = "UNSET_REQUIRED_FIELD"
		else:
			self.vlantype = self.VLAN_TYPE_NPV

#################################################################
#Start procedural code here
#################################################################
giftypes = iface.__subclasses__()
goptions = None
gargs = None

def createNewConfig(ifname, iftype):
	global giftypes

	for i in giftypes:
		if (i.iftypename == iftype):
			newif = i(ifname)
			newif.create()
	
def log(msg):
	if (goptions.quiet == False):
		print(msg)

def main():
	global goptions
	global gargs
	global giftypes

	parser = OptionParser()

	group = OptionGroup(parser, "Manditory Options")

	group.add_option("-i", "--interface", dest="iface",
		help="Name of the interface file being modified")

	parser.add_option_group(group)

	group = OptionGroup(parser, "Miscelaneous Options")

	group.add_option("-c", "--create", dest="create",
		default=False, action="store_true",
		help="Create a new config file, default is to modify a config")

	helpstr = "Type of interface to create: [ "
	for i in giftypes:
		helpstr += i.iftypename + " "
	helpstr += "]\n Default is " + iface.iftypename

	group.add_option("-t", "--type", dest="iftype", default="eth",
		help=helpstr)

	group.add_option("-q", "--quiet", dest="quiet",
		default=False, action="store_true",
		help="Don't print anything extraneous to the console")

	group.add_option("-d", "--dump", dest="dump",
		default=False, action="store_true",
		help="Dump the contents of the config file")

	group.add_option("-s", "--set", dest="newvals",
		action="append",
		help="Set a new key/value pair for the config"
		     "Note setting a key with no '=' removes the key"
		     "Examples:"
		     "-s KEY=VAL (adds key with value VAL to the config)\n"
		     "-s KEY (removes key from the config")

	parser.add_option_group(group)

	group = OptionGroup(parser, "Bonding Options")
	group.add_option("--enslave", dest="enslave", action="append", 
		help="Enslave the specified interface")
	group.add_option("--deslave", dest="deslave", action="append",
		help="Deslave the specified interface")
	group.add_option("--list-slaves", dest="listslaves", default=False,
		action="store_true", help="list enslaved devices")

	parser.add_option_group(group)

	(options, args) = parser.parse_args()

	goptions = options
	gargs = args

	if (goptions.iface == None):
		log("You must specify an interface with -i")
		sys.exit(1)

	if (goptions.create == True):
		log("Creating new config")
		try:
			createNewConfig(goptions.iface, goptions.iftype)
		except IfaceError as e:
			log("Cant create new iface " + goptions.iface)
			log(e.errmsg)
			sys.exit(2)

	ifdev = iface(goptions.iface);
	try:
		ifdev.load()
		ifdev = ifdev.getSpecificIfType()
	except IfaceError as e:
		log("Can't load iface " + goptions.iface)
		log(e.errmsg)
		sys.exit(3)

	if (goptions.dump == True):
		ifdev.dump()
		sys.exit(0)

	try:
		if (len(goptions.newvals)):
			ifdev.update(goptions.newvals)
	except TypeError:
		pass

	try:
		if (len(goptions.enslave)):
			for slave in goptions.enslave:
				ifdev.enslave(slave)
	except TypeError:
		# If we're not doing any enslaving, just let it go
		pass

	except IfaceError as e:
		log("Failed an enslave operation: " + e.errmsg)
		sys.exit(4)

	except AttributeError:
		log("Interfaces that are not bond masters can't enslave")
		sys.exit(5)

	try:
		if (len(goptions.deslave)):
			for slave in goptions.deslave:
				ifdev.deslave(slave)
	except TypeError:
		# If we're not doing any enslaving, just let it go
		pass
	except IfaceError as e:
		log("Failed an deslave operation: " + e.errmsg)
		sys.exit(4)
	except AttributeError:
		log("Interfaces that are not bond masters can't deslave")
		sys.exit(5)

	if (goptions.listslaves == True):
		try:
			for slave in ifdev.slaves:
				print(slave.name)
		except AttributeError:
			log("Only bond master devices have slaves")
			sys.exit(6)

	ifdev.save()

	sys.exit(0)	
if __name__ == "__main__":
	main()

