#!/usr/bin/python3

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 __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:
			return None

	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 lines[0][0:4] != hex(self.vendor)[2:]:
						f.close()
						continue
					f.close()
					f = open(os.path.join(root, d, "idProduct"))
					lines = f.readlines()
					if lines[0][0:4] != hex(self.product)[2:]:
						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 = None
		self.mask = None
		self.network = None

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

	#
	# 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"
		if self.assigntype == AssignType.STATIC:
			if self.address.version == 4:
				methodp = "ipv4.method"
				addrp = "ipv4.addresses"	
			else:
				methodp = "ipv6.method"
				addrp = "ipv6.addresses"
		try:
			nmcon.update_property(methodp, assignmap[self.assigntype])
			if self.assigntype == AssignType.STATIC:
				nmcon.update_property(addrp, str(self.address) + "/" + str(self.network.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 = None
		self.mask = None
		try:
			cursor = cursor_consume_next(cursor, "Redfish Service IP Discovery Type: ")
			if cursor == None:
				print("redfish-finder: Unable to find Redfish Service Info")
				return None
			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 = ipaddress.IPv4Address(cursor.split()[0])
					cursor = cursor_consume_next(cursor, "IPv4 Redfish Service Mask: ")
					self.mask = ipaddress.IPv4Address(cursor.split()[0])
				elif cursor.split()[0] == "IPv6":
					cursor = cursor_consume_next(cursor, "IPv6 Redfish Service Address: ")
					self.address = ipaddress.IPv6Address(unicode(cursor.split()[0], "utf-8"))
					cursor = cursor_consume_next(cursor, "IPv6 Mask: ")
					self.mask = 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: ")
			self.hostname = cursor.split()[0]
		except:
			print("redfish-finder: Unexpected error parsing ServiceConfig")

	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):
		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):
			return None
		cursor = cursor_consume_next(cursor, "Host Interface Type: Network\n")
		if (cursor == None):
			return None

		# 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"):
			self.device = USBNetDevice(cursor)

		if self.device == None:
			return None

		# Now find the Redfish over IP section
		cursor = cursor_consume_next(cursor, "Protocol ID: 04 (Redfish over IP)\n")
		if (cursor == None):
			print("redfish-finder: Unable to find Redfish Protocol")
			return None

		self.hostconfig = HostConfig(cursor)
		if self.hostconfig == None:
			return None

		self.serviceconfig = ServiceConfig(cursor)
		if self.serviceconfig == None:
			return None


	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
		newentry = str(self.sconf.address) + "     " + 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:
				print("redfish-finder: Unexpected error building connection to %s" % self.ifc)
				return None

		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:
			print("Unexpected error parsing connection for %s" % self.ifc)
			return None

	#
	# 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():
	dmioutput = subprocess.check_output(["/usr/sbin/dmidecode", "-t42"])
	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")
	smbios_info = get_info_from_dmidecode()
	if smbios_info == None:
		sys.exit(1)
	print("redfish-finder: Building NetworkManager connection info")
	conn = nmConnection(smbios_info.device)
	if conn == None:
		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()


