#!/usr/libexec/platform-python

import sys
import os
import subprocess
import ipaddress
import argparse

#######################################################
# Helper function to consume the dmidecode output
# Accepts a string as input and truncates the string
# to the point immediately following needle
######################################################
def cursor_consume_next(cursor, needle):
	idx = cursor.find(needle)
	if idx == -1:
		return None
	return cursor[idx + len(needle):]

######################################################
# The AssignType class.  Ennumerates the various
# Types of Host and Redfish  IP Address assignments
# That are possible. Taken from the Redfish Host API 
# Specification 
######################################################
class AssignType():
	UNKNOWN = 0
	STATIC = 1
	DHCP = 2
	AUTOCONF = 3
	HOSTSEL = 4

	typestring = ["Unknwon", "Static", "DHCP", "Autoconf", "Host Selected"]


######################################################
# NetDevice class, Superclass of bus specific Device Classes
# USBNetDevice, and PCINetDevice.  Provides the getifcname
# interface for use in configuring the OS via nmcli
######################################################
class NetDevice(object):
	def __init(self):
		self.name = "Unknown"

	#
	# Superclass function to get OS interface name
	# agnostic to Physical interface type
	#
	def getifcname(self):
		return self.name

	def merge(self, newdev):
		return self

	def __str__(self):
		return "Interface: " + self.name

######################################################
# Subclass of NetDevice, parses the dmidecode output
# to discover interface name for USB type devices
######################################################
class USBNetDevice(NetDevice):
	def __init__(self, dmioutput):
		super(NetDevice, self).__init__()
		dmioutput = cursor_consume_next(dmioutput, "idVendor: ")
		# Strip the 0x off the front of the vendor and product ids
		self.vendor = int((dmioutput.split()[0])[2:], 16)
		self.product = int((dmioutput.split()[2])[2:], 16)

		# Now we need to find the corresponding device name in sysfs
		if self._find_device() == False:
			raise RuntimeError("Unable to find USB network device")

	def _getname(self, dpath):
		for root, dirs, files in os.walk(dpath, topdown=False):
			for d in dirs:
				try:
					if d != "net":
						continue
					dr = os.listdir(os.path.join(root, d))
					self.name = dr[0]
					return True
				except:
					continue
		return False

	def _find_device(self):
		for root,dirs,files in os.walk("/sys/bus/usb/devices", topdown=False):
			for d in dirs:
				try:
					f = open(os.path.join(root, d, "idVendor"))
					lines = f.readlines()
					if int(lines[0][0:4],16) != self.vendor:
						f.close()
						continue
					f.close()
					f = open(os.path.join(root, d, "idProduct"))
					lines = f.readlines()
					if int(lines[0][0:4],16) != self.product:
						f.close()
						continue
					f.close()
					# We found a matching product and vendor, go find the net directory
					# and get the interface name
					return self._getname(os.path.join(root, d))
				except:
					continue
		print("redfish-finder: Unable to find usb network device with vendor:product %s:%s" % (hex(self.vendor), hex(self.product)))
		return False 


######################################################
# Parses out HostConfig information from SMBIOS for use in
# Configuring OS network interface 
######################################################
class HostConfig():
	def __init__(self, cursor):
		self.address = [] 
		self.mask = [] 
		self.network = [] 

		try:
			cursor = cursor_consume_next(cursor, "Host IP Assignment Type: ")
			if cursor == None:
				raise RuntimeError("redfish-finder: Unable to parse SMBIOS Host IP Assignment Type")
			if cursor.split()[0] == "Static":
				self.assigntype = []
				self.assigntype.append(AssignType.STATIC)
				cursor = cursor_consume_next(cursor, "Host IP Address Format: ")
				if cursor.split()[0] == "IPv4":
					cursor = cursor_consume_next(cursor, "IPv4 Address: ")
					addr = cursor.split()[0]
					self.address.append(ipaddress.IPv4Address(addr))
					cursor = cursor_consume_next(cursor, "IPv4 Mask: ")
					mask = cursor.split()[0]
					self.mask.append(ipaddress.IPv4Address(mask))
					self.network.append(ipaddress.IPv4Network(addr + "/" + mask, strict=False))
				elif cursor.split()[0] == "IPv6":
					cursor = cursor_consume_next(cursor, "IPv6 Address: ")
					addr = cursor.split()[0]	
					self.address.append(ipaddress.IPv6Address(addr))
					cursor = cursor_consume_next(cursor, "IPv6 Mask: ")
					mask = cursor.split()[0]
					self.mask.append(ipaddress.IPv4Address(mask))
					self.network.append(ipaddress.IPv6Network(addr + "/" + mask, strict=False))
			elif cursor.split()[0] == "DHCP":
				self.assigntype.append(AssignType.DHCP)
				self.address.append(0)
				self.mask.append(0)
				self.network.append(0)
			else:
				# Support the other types later
				raise RuntimeError("redfish-finder: Unable to parse SMBIOS Host configuaration")
		except:
			raise RuntimeError("redfish-finder: Unexpected error while parsing HostConfig!")

	def merge(self, newconfig):
		self.assigntype.extend(newconfig.assigntype)
		self.address.extend(newconfig.address)
		self.mask.extend(newconfig.mask)
		self.network.extend(newconfig.network)
		return self

	#
	# Using the smbios host config info, set the appropriate
	# attributes of the network manager connection object
	#
	def generate_nm_config(self, device, nmcon):
		assignmap = { AssignType.STATIC: "manual", AssignType.DHCP: "auto"}
		methodp = "ipv4.method"
		for i in range(len(self.assigntype)):
			assigntype = self.assigntype[i]
			if assigntype == AssignType.STATIC:
				if self.address[i].version == 4:
					methodp = "ipv4.method"
					addrp = "ipv4.addresses"	
				else:
					methodp = "ipv6.method"
					addrp = "ipv6.addresses"
			try:
				nmcon.update_property(methodp, assignmap[assigntype])
				if assigntype == AssignType.STATIC:
					nmcon.update_property(addrp, str(self.address[i]) + "/" + str(self.network[i].prefixlen))
			except:
				print("redfish-finder: Error generating nm_config")
				return False

		return True


	def __str__(self):
		val = "Host Config(" + AssignType.typestring[self.assigntype] + ")" 
		if (self.assigntype == AssignType.STATIC):
			val = val + " " + str(self.address) + "/" + str(self.mask)
		return val


######################################################
# Class to hold Redfish service information, as extracted 
# From the smbios data from dmidecode
######################################################
class ServiceConfig():
	def __init__(self, cursor):
		self.address = []
		self.mask = []
		try:
			cursor = cursor_consume_next(cursor, "Redfish Service IP Discovery Type: ")
			if cursor == None:
				raise RuntimeError("redfish-finder: Unable to find Redfish Service Info")
			if cursor.split()[0] == "Static":
				self.assigntype = AssignType.STATIC
				cursor = cursor_consume_next(cursor, "Redfish Service IP Address Format: ")
				if cursor.split()[0] == "IPv4":
					cursor = cursor_consume_next(cursor, "IPv4 Redfish Service Address: ")
					self.address.append(ipaddress.IPv4Address(cursor.split()[0]))
					cursor = cursor_consume_next(cursor, "IPv4 Redfish Service Mask: ")
					self.mask.append(ipaddress.IPv4Address(cursor.split()[0]))
				elif cursor.split()[0] == "IPv6":
					cursor = cursor_consume_next(cursor, "IPv6 Redfish Service Address: ")
					self.address.append(ipaddress.IPv6Address(unicode(cursor.split()[0], "utf-8")))
					cursor = cursor_consume_next(cursor, "IPv6 Mask: ")
					self.mask.append(ipaddress.IPv4Address(unicode(cursor.split()[0], "utf-8")))
			elif cursor.split()[0] == "DHCP":
				self.assigntype = AssignType.DHCP
			else:
				# Support the other types later
				print("redfish-finder: Unable to parse SMBIOS Service Config info")
				return None
			cursor = cursor_consume_next(cursor, "Redfish Service Port: ")
			self.port = int(cursor.split()[0])
			cursor = cursor_consume_next(cursor, "Redfish Service Vlan: ")
			self.vlan = int(cursor.split()[0])
			cursor = cursor_consume_next(cursor, "Redfish Service Hostname: ")

			#
			# Sanity check: If it contains the consecutive spaces
			# only, reference to the index '0' will throw an
			# exception.
			#
			if len(cursor.split()) != 0:
				self.hostname = cursor.split()[0]
			else:
				self.hostname = ""
		except:
			raise RuntimeError("redfish-finder: Unexpected error parsing ServiceConfig")

	def merge(self, newconfig):
		self.address.extend(newconfig.address)
		self.mask.extend(newconfig.mask)
		return self

	def __str__(self):
		val = "Service Config(" + AssignType.typestring[self.assigntype] + ")" 
		if (self.assigntype == AssignType.STATIC):
			val = val + " " + str(self.address) + "/" + str(self.mask)
		return val

######################################################
# Object to hold the information parsed from smbios
# Split into a Netdevice object, a HostConfig object
# and a ServiceConfig object
######################################################
class dmiobject():
	def __init__(self, dmioutput):
		self.device = None
		self.hostconfig = None
		self.serviceconfig = None
		cursor = dmioutput
		# Find the type 42 header, if not found, nothing to do here
		cursor = cursor_consume_next(cursor, "Management Controller Host Interface\n")
		if cursor == None:
			raise RuntimeError("Unable to find Management Controller Host Interface block in SMBIOS")
		while (cursor != None):
			if (cursor == None):
				raise RuntimeError("No Redfish Host support detected")
			cursor = cursor_consume_next(cursor, "Host Interface Type: Network\n")
			if (cursor == None):
				raise RuntimeError("No supported Redfish host interface type found")

			# If we get here then we know this is a network interface device
			cursor = cursor_consume_next(cursor, "Device Type: ")
			# The next token should either be:
			# USB
			# PCI/PCIe
			# OEM
			# Unknown
			dtype = cursor.split()[0]
			if (dtype == "USB"):
				newdev = USBNetDevice(cursor)

			if self.device == None:
				self.device = newdev
			else:
				self.device.merge(newdev)

			# Now find the Redfish over IP section
			cursor = cursor_consume_next(cursor, "Protocol ID: 04 (Redfish over IP)\n")
			if (cursor == None):
				raise RuntimeError("No Redfish over IP Protocol support")

			newhostconfig = HostConfig(cursor)

			if self.hostconfig == None:
				self.hostconfig = newhostconfig
			else:
				self.hostconfig.merge(newhostconfig)

			serviceconfig = ServiceConfig(cursor)

			if self.serviceconfig == None:
				self.serviceconfig = serviceconfig
			elif self.serviceconfig == None:
				self.serviceconfig.merge(serviceconfig)

			cursor = cursor_consume_next(cursor, "Management Controller Host Interface\n")


	def __str__(self):
		return str(self.device) + " | " + str(self.hostconfig) + " | " + str(self.serviceconfig)

######################################################
# Service Data represents the config data that gets written to 
# Various OS config files (/etc/host) 
######################################################
class OSServiceData():
	def __init__(self, sconf):
		self.sconf = sconf
		try:
			f = open("/etc/hosts", "r")
			self.host_entries = f.readlines()
			self.constant_name = "redfish-localhost"
			f.close()
		except:
			print("Unable to read OS Config Files")
			return None

	#
	# Method to read in /etc/hosts, remove old redfish entries
	# and insert new ones based on ServiceConfig
	#
	def update_redfish_info(self):
		# strip any redfish localhost entry from host_entries
		# as well as any entries for the smbios exported host name
		for h in self.host_entries:
			if h.find(self.constant_name) != -1:
				self.host_entries.remove(h)
				continue
			if h.find(self.sconf.hostname) != -1:
				self.host_entries.remove(h)
				continue

		# Now add the new entries in
		addresses=""
		for i in self.sconf.address:
			addresses = addresses + str(i) + " "
		newentry = addresses + "     " + self.constant_name
		newentry = newentry + " " + self.sconf.hostname
		self.host_entries.append(newentry)

	#
	# write out /etc/hosts with updated redfish info
	#
	def output_redfish_config(self):
		try:
			f = open("/etc/hosts", "w")
			f.writelines(self.host_entries)
			f.close()
		except:
			print("Unalbe to open OS Config Files for writing")
			return False
		return True

	#
	# Remove redfish information from /etc/hosts, and write it back to file
	#
	def remove_redfish_config(self):
		for h in self.host_entries:
			if h.find(self.constant_name) != -1:
				self.host_entries.remove(h)
				continue
			if h.find(self.sconf.hostname) != -1:
                                self.host_entries.remove(h)
                                continue
		return self.output_redfish_config()

######################################################
# Represents an nm connection, pulls in the config from nmi con show <ifc>
# Creates the connection if it doesn't exist, and updates it to reflect the host 
# Config data from the HostConfig class
######################################################
class nmConnection():
	def __init__(self, ifc):
		self.ifc = ifc
		self.cmdlinedown = "nmcli con down id " + self.ifc.getifcname()
		self.cmdlineup = "nmcli con up id " + self.ifc.getifcname()

		try:
			propstr = subprocess.check_output(["nmcli", "con", "show", ifc.getifcname()])
		except:
			# Need to create a connection
			try:
				create = 	[ "nmcli", "con", "add",
						"connection.id", ifc.getifcname(),
						"connection.type", "802-3-ethernet",
						"connection.interface-name", ifc.getifcname()]

				subprocess.check_call(create)
				propstr = subprocess.check_output(["nmcli", "con", "show", ifc.getifcname()])
			except:
				raise RuntimeError("redfish-finder: Unexpected error building connection to %s" % self.ifc)

		try:
			self.properties = {}
			self.updates = [] 
			lines = propstr.splitlines()
			for l in lines:
				la = l.decode("utf-8").split()
				if len(la) < 2:
					continue
				self.properties[la[0].strip(":")] = la[1]
		except:
			raise RuntimeError("Unexpected error parsing connection for %s" % self.ifc)

	#
	# Update a property value in this network manager object
	#
	def update_property(self, prop, val):
		if self.get_property(prop) == val:
			return
		self.properties[prop] = val
		self.updates.append(prop)

	#
	# Read a network manager object property
	#
	def get_property(self, prop):
		return self.properties[prop]

	#
	# Using this object, run nmcli to update the os so that the
	# interface represented here is in sync with our desired
	# configuration
	#
	def sync_to_os(self):
		cmdlinemod = "nmcli con modify id " + self.ifc.getifcname() + " "
		for i in self.updates:
			cmdlinemod = cmdlinemod + i + " " + self.properties[i] + " "
		try:
			subprocess.check_call(self.cmdlinedown.split())
		except subprocess.CalledProcessError as e:
			if e.returncode != 10:
				print("redfish-finder: Unable to take down interface: error %d" % e.returncode)
				return False
		except:
			print("redfish-finder: Unexpected error running nmcli while dropping connection")
			return False

		try:
			if len(self.updates) != 0:
				subprocess.check_call(cmdlinemod.split())
			subprocess.check_call(self.cmdlineup.split())
		except:
			print("redfish-finder: Unexpected error running nmcli while updating connection")
			return False
		return True

	#
	# Down the represented interface
	#
	def shutdown(self):
		try:
			subprocess.check_call(self.cmdlinedown.split())
		except:
			print("redfish-finder: Unexpected error running nmcli while closing connection")
			return False
		return True

	def __str__(self):
		return str(self.properties)

def get_info_from_dmidecode():
	try:
		dmioutput = subprocess.check_output(["/usr/sbin/dmidecode", "-t42"])
	except:
		raise RuntimeError("Failed to run dmidecode.  Make sure you run as root")
	return dmiobject(dmioutput.decode())

def main():
	parser = argparse.ArgumentParser(description='Use SMBios info to setup canonical BMC access from the host')
	parser.add_argument('-s', '--shutdown', action='store_true')
	args = parser.parse_args(sys.argv[1:])

	print("redfish-finder: Getting dmidecode info")
	try:
		smbios_info = get_info_from_dmidecode()
	except RuntimeError as e:
		print("Error parsing dmidecode information: %s\n" % e)
		sys.exit(1)

	print("redfish-finder: Building NetworkManager connection info")
	try:
		conn = nmConnection(smbios_info.device)
	except RuntimeError as e:
		print("redfish-finder: Error building nm connection: %s\n" % e)
		sys.exit(1)

	print("redfish-finder: Obtaining OS config info")
	svc = OSServiceData(smbios_info.serviceconfig)
	if svc == None:
		sys.exit(1)

	if args.shutdown == True:
		print("Shutting down interface %s" % smbios_info.device.getifcname())
		conn.shutdown()
		print("Scrubbing host info from OS config")
		svc.remove_redfish_config()
		sys.exit(0)

	print("redfish-finder: Converting SMBIOS Host Config to NetworkManager Connection info")
	if smbios_info.hostconfig.generate_nm_config(smbios_info.device, conn) == False:
		sys.exit(1)
	print("redfish-finder: Applying NetworkManager connection configuration changes")
	if conn.sync_to_os() == False:
		sys.exit(1)
	print("redfish-finder: Adding redfish host info to OS config")
	svc.update_redfish_info()
	if svc.output_redfish_config() == False:
		sys.exit(1)
	print("redfish-finder: Done, BMC is now reachable via hostname redfish-localhost")

if __name__ == "__main__":
	main()


