#!/usr/libexec/platform-python

################################################################################
# serial45d - used to store serial, model and chassis information on 
#          45Drives storage servers. 
#
# Copyright (C) 2021, Mark Hooper   <mhooper@45drives.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#   
################################################################################
import re
import subprocess
import os
import datetime

g_config_file_content = {
	"Motherboard":[],
	"CPU":[],
	"Chassis Type":[],
	"Product Type":[],
	"Product Name":[]
}

g_update_config = False

g_current_dir = os.path.dirname(os.path.realpath(__file__))

g_progress_step = 0

def outputSerialVersion():
	print("\n")
	print("-----------------------------------------------------------------------------------")
	print("                                                                                   ")
	print("                                $$\           $$\ $$\   $$\ $$$$$$$\  $$$$$$$\     ")
	print("                                \__|          $$ |$$ |  $$ |$$  ____| $$  __$$\    ")
	print("   $$$$$$$\  $$$$$$\   $$$$$$\  $$\  $$$$$$\  $$ |$$ |  $$ |$$ |      $$ |  $$ |   ")
	print("  $$  _____|$$  __$$\ $$  __$$\ $$ | \____$$\ $$ |$$$$$$$$ |$$$$$$$\  $$ |  $$ |   ")
	print("  \$$$$$$\  $$$$$$$$ |$$ |  \__|$$ | $$$$$$$ |$$ |\_____$$ |\_____$$\ $$ |  $$ |   ")
	print("   \____$$\ $$   ____|$$ |      $$ |$$  __$$ |$$ |      $$ |$$\   $$ |$$ |  $$ |   ")
	print("  $$$$$$$  |\$$$$$$$\ $$ |      $$ |\$$$$$$$ |$$ |      $$ |\$$$$$$  |$$$$$$$  |   ")
	print("  \_______/  \_______|\__|      \__| \_______|\__|      \__| \______/ \_______/    ")
	ver_file_path = f"{g_current_dir}/configs/version"
	if(os.path.exists(ver_file_path) and os.path.isfile(ver_file_path)):
		ver_file = open(ver_file_path,"r")
		version = ver_file.readline()
		ver_file.close()
		
		print("\n                                                                  Version: {v}".format(v=version))
		print("-----------------------------------------------------------------------------------\n")
	else:
		print("\n-----------------------------------------------------------------------------------\n")
	
		

##################################################################################
# getMotherboard()
# performs the command: dmidecode -t 2
# searches the output for the motherboard model and serial number and 
# returns them as strings.
##################################################################################
def getMotherboard():
	global g_update_config
	global g_config_file_content
	board_model = "unknown"
	board_serial = "unknown"
	board_manufacturer = "unknown"
	try:
		dmi_result = subprocess.Popen([f"{g_current_dir}/dependencies/dmidecode/dmidecode","-t","2"],stdout=subprocess.PIPE,universal_newlines=True).stdout
	except:
		print("Error Executing dmidecode")
		exit(1)
	for line in dmi_result:
			regex_board_model = re.search("^\sProduct Name:\s+(.*)",line)
			regex_board_serial = re.search("^\sSerial Number:\s+(.*)",line)
			regex_board_manufacturer = re.search("^\sManufacturer:\s+(.*)",line)
			if regex_board_model != None:
				if regex_board_model.group(1) not in g_config_file_content["Motherboard"]:
					# board was not found in the config file, add it and set the 
					# g_update_config flag. 
					g_config_file_content["Motherboard"].append(regex_board_model.group(1))
					g_update_config = True
				board_model = regex_board_model.group(1)
			if regex_board_serial != None:
				board_serial = regex_board_serial.group(1)
			if regex_board_manufacturer != None:
				board_manufacturer = regex_board_manufacturer.group(1)
			if board_model != "unknown" and board_serial != "unknown" and board_manufacturer != "unknown":
				# all information has been assigned, break from the loop
				break
	return board_model,board_serial,board_manufacturer

##################################################################################
# getCPU()
# performs the command: dmidecode -t 4
# searches the output for the cpu model, and maintains a count of cpu's found. 
# Will also ensure that the config file is updated if a cpu is detected that
# was not already present in the config file by setting the g_update_config flag.
##################################################################################
def getCPU():
	global g_update_config
	global g_config_file_content
	cpu_model = "unknown"
	cpu_count = 0
	try:
		dmi_result = subprocess.Popen(["dmidecode","-t","4"],stdout=subprocess.PIPE,universal_newlines=True).stdout
	except:
		print("Error Executing dmidecode")
		exit(1)
	for line in dmi_result:
		regex_cpu = re.search("^\sVersion:\s+(.*)$",line)
		if regex_cpu != None:
			regex_cpu_version = str(regex_cpu.group(1)).rstrip()
			if regex_cpu_version not in g_config_file_content["CPU"]:
				# board was not found in the config file, add it and set the 
				# g_update_config flag. 
				g_config_file_content["CPU"].append(regex_cpu_version)
				g_update_config = True
			cpu_model = regex_cpu_version
			cpu_count += 1
		if cpu_count == 2:
			# we have found all possible entries, stop searching
			break
	return cpu_model, cpu_count

##################################################################################
# getHBA()
# performs the command: ...storcli64 show all
# counts the number of 16i and 24i hba cards and returns these as integers
##################################################################################
def getHBA():

	hba_models = {
		"SAS9305-16i":0,
		"SAS9305-24i":0
	}

	try:
		storcli64_result = subprocess.Popen(
			[f"{g_current_dir}/dependencies/storcli64/storcli64","show","all"],stdout=subprocess.PIPE,universal_newlines=True)
	except:
		print("Error running storcli64")
		exit(1)
	card_count = 0
	for line in storcli64_result.stdout:
		for model in hba_models.keys():
			# Model AdapterType VendId DevId SubVendId SubDevId PCIAddress 	
			regex = re.search("({mdl}).*00:\w\w:\w\w:\w\w\s+$".format(mdl=model),line)
			if regex != None:
				hba_models[model] += 1
				
	return hba_models["SAS9305-16i"], hba_models["SAS9305-24i"]


##################################################################################
# loadSystemConfigs()
# This function will load supported configs from an included file
# in ./configs/configs.txt
# This way, if we discover new configurations, we can save them by updating
# the config files.
##################################################################################
def loadSystemConfigs():
	config_file = open(f"{g_current_dir}/configs/config.txt","r")
	for line in config_file:
		for key in g_config_file_content.keys():
			regex = re.search("({k}):(.*)$".format(k=key),line)
			if regex != None:
				g_config_file_content[regex.group(1)].append(regex.group(2))

##################################################################################
# performFRUWrite()
# This function creates the fru.ini file, and calls fru-tool to generate 
# the FRU encoded binary file. Then calls ipmitool fru write using that binary
# file. 
##################################################################################
def performFRUWrite(ipmitool_information,fru_fields):
	print("\n")
	print("+----------------------------------------------------------------+")
	print("| Step {sn}: Perform Final Write Operation                          |".format(sn=updateProgressStep()))
	print("+----------------------------------------------------------------+")

	# adjust the chassis_part_number and version fields
	# to indicate that the system is a hybrid
	if "H16" in fru_fields["product_name"]:
		fru_fields["chassis_part_number"] = "H16-" + fru_fields["part_number"]
		fru_fields["version"] = "Hybrid16(" + fru_fields["version"] + ")"
	elif "H32" in fru_fields["product_name"]:
		fru_fields["chassis_part_number"] = "H32-" + fru_fields["part_number"]
		fru_fields["version"] = "Hybrid32(" + fru_fields["version"] + ")"
	else:
		fru_fields["chassis_part_number"] = fru_fields["part_number"]
		fru_fields["version"] = "(" + fru_fields["version"] + ")"
	
	# assign the remaining fields.
	fru_fields["chassis_serial"] = fru_fields["serial"]

	fru_structure = {
		'common': {
			'version': 1,
			'size': ipmitool_information.get("FRU Size","256"),
			'internal': 0,
			'chassis': 1,
			'board': 1,
			'product': 1,
			'multirecord': 0,
		},
		'chassis': {
			'type': 17,
			'part': fru_fields["chassis_part_number"],
			'serial': fru_fields['chassis_serial'],
		},
		'board': {
			'language': 0,
			'date': ipmitool_information.get("Board Mfg Date Hex",""),
			'manufacturer': ipmitool_information.get("Board Mfg",""),
			'product': ipmitool_information.get("Board Product",""),
			'serial': ipmitool_information.get("Board Serial",""),
			'part': ipmitool_information.get("Board Part Number",ipmitool_information.get("Board Product","")),
			'fileid': '',
		},
		'product': {
			'language': 0,
			'manufacturer': fru_fields["manufacturer_name"],
			'product': fru_fields["product_name"],
			'part': fru_fields["part_number"],
			'version': fru_fields["version"],
			'serial': fru_fields['serial'],
			'asset': fru_fields["asset_tag"],
			'fileid': '',
		},
	}

	# get timestamp for ini filename.
	date = datetime.datetime.now()
	timestamp = date.strftime("%Y-%m-%d_%H-%M-%S")
	product_serial = fru_fields["serial"]

	# create .ini input file directory 
	ini_dir = f"{g_current_dir}/fru_ini"
	if not os.path.exists(ini_dir):
		print(f"making directory to store fru config files in {ini_dir}")
		os.makedirs(ini_dir)

	ini_name = f"{ini_dir}/fru-{product_serial}-{timestamp}.ini".replace(" ", "")

	# create output directory 
	bin_dir = f"{g_current_dir}/fru_bin"
	if not os.path.exists(bin_dir):
		print(f"making directory to store fru-tool generated bin files in {bin_dir}")
		os.makedirs(bin_dir)

	bin_name = f"{bin_dir}/fru-{product_serial}-{timestamp}.bin".replace(" ", "")

	# .ini file creation
	fru_ini_lines = []
	for section in fru_structure.keys():
		fru_ini_lines.append(f"[{section}]")

		for field in fru_structure[section].keys():
				value = fru_structure[section][field]
				fru_ini_lines.append(f"{field} = {value}")
		fru_ini_lines.append('')

	ini_file = open(ini_name, "w")
	for line in fru_ini_lines:
		ini_file.write(f"{line}\n")
	ini_file.close()

	# binary file generation
	bin_generate_result = None
	try:
		bin_generate_result = subprocess.run([f"{g_current_dir}/fru-tool", ini_name, bin_name],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,universal_newlines=True)
	except:
		print(f"!!!! Error performing bin file generation ({g_current_dir}/fru-tool) !!!!")
		print(bin_generate_result.stdout)
		exit(1)

	# write final information using fru-tool (added 2022-08-22)
	arg_vector = ["ipmitool", "fru", "write", "0", bin_name]

	if "supermicro" in fru_fields["board_manufacturer"].lower():
		arg_vector = [
			f"{g_current_dir}/dependencies/ipmi_linux/IPMICFG-Linux.x86_64",
			"-fru",
			"restore",
			bin_name
		]

	fru_write_result = None
	try:
		fru_write_result = subprocess.Popen(arg_vector,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,universal_newlines=True)
	except:
		print("!!!! Error performing write !!!!")
		for i in range(0,len(arg_vector)):
			print(arg_vector[i],end=" ")
		exit(1)

	# output the result of the write operation
	print("\n!!!!   Performing Write  !!!!\n")
	print("\n Output from {prg}: ".format(prg=arg_vector[0]))
	for line in fru_write_result.stdout:
		print("\t" + line,end="")

	# run ipmitool fru again and present the output to the user.
	ipmitool_fru_result = None
	try:
		ipmitool_fru_result = subprocess.Popen(
			["ipmitool","fru"],stdout=subprocess.PIPE,universal_newlines=True)
	except:
		print("Error executing \"ipmitool fru\"")
		exit(1)	

	# output the result of the write operation
	print("\n")
	print("+----------------------------------------------------------------+")
	print("| Step {sn}: Verify Output                                          |".format(sn=updateProgressStep()))
	print("+----------------------------------------------------------------+")
	print("\nOutput from ipmitool fru: ")
	for line in ipmitool_fru_result.stdout:
		print("\t" + line,end="")
	print("Setup Complete. If the results shown above appear incorrect") 
	print("run this program again, or contact 45Drives Support.")
	input("Press Enter to end program.") 

##################################################################################
# performFRUWriteSuperMicro()
# This function adjusts the remaining fru fields 
# and performs the final write operation by using
# ./dependencies/ipmi_linux/IPMICFG-Linux.x86_64 from supermicro.
##################################################################################
def performFRUWriteSuperMicro(fru_fields):
	print("\n")
	print("+----------------------------------------------------------------+")
	print("| Step {sn}: Perform Final Write Operation                          |".format(sn=updateProgressStep()))
	print("+----------------------------------------------------------------+")

	# get the current date for the manufacturing date
	date = datetime.datetime.now()
	fru_fields["board_manufacturing_date"] = date.strftime("%Y%m%d%H%M")

	# adjust the chassis_part_number and version fields
	# to indicate that the system is a hybrid
	if "H16" in fru_fields["product_name"]:
		fru_fields["chassis_part_number"] = "H16-" + fru_fields["part_number"]
		fru_fields["version"] = "Hybrid16(" + fru_fields["version"] + ")"
	elif "H32" in fru_fields["product_name"]:
		fru_fields["chassis_part_number"] = "H32-" + fru_fields["part_number"]
		fru_fields["version"] = "Hybrid32(" + fru_fields["version"] + ")"
	else:
		fru_fields["chassis_part_number"] = fru_fields["part_number"]
		fru_fields["version"] = "(" + fru_fields["version"] + ")"
	
	# assign the remaining fields.
	fru_fields["chassis_serial"] = fru_fields["serial"]
	fru_fields["chassis_type"] = "17h"

	# prepare arguments for function call
	fru_args = [
		f"{g_current_dir}/dependencies/ipmi_linux/IPMICFG-Linux.x86_64",
		"-fru",
		"dmi",
		fru_fields["manufacturer_name"],
		fru_fields["product_name"],
		fru_fields["part_number"],
		fru_fields["version"],
		fru_fields["serial"],
		fru_fields["asset_tag"],
		fru_fields["board_manufacturing_date"],
		fru_fields["board_manufacturer"],
		fru_fields["board_model"],
		fru_fields["board_model"],
		fru_fields["board_serial"],
		fru_fields["chassis_type"],
		fru_fields["chassis_part_number"],
		fru_fields["chassis_serial"]
	]

	# perform the function call
	try:
		ipmi_result = subprocess.Popen(
			fru_args,stdout=subprocess.PIPE,universal_newlines=True)
	except:
		print("!!!! Error performing write !!!!")
		for i in range(0,len(fru_args)):
			print(fru_args[i],end=" ")
		exit(1)

	# output the result of the write operation
	print("\n!!!!   Performing Write  !!!!\n")
	print("\n Output from ipmicfg: ")
	for line in ipmi_result.stdout:
		print("\t" + line,end="")

	try:
		ipmitool_result = subprocess.Popen(
			["ipmitool","fru"],stdout=subprocess.PIPE,universal_newlines=True)
	except:
		print("Error executing \"ipmitool fru\"")
		exit(1)	

	ipmitool_fru_output = []
	# output the result of the write operation
	print("\n")
	print("+----------------------------------------------------------------+")
	print("| Step {sn}: Verify Output                                          |".format(sn=updateProgressStep()))
	print("+----------------------------------------------------------------+")
	print("\nOutput from ipmitool fru: ")
	for line in ipmitool_result.stdout:
		print("\t" + line,end="")
		ipmitool_fru_output.append(line)
	print("Setup Complete. If the results shown above appear incorrect") 
	print("run this program again, or contact 45Drives Support.")
	input("Press Enter to end program.") 
	return ipmitool_fru_output

##################################################################################
# countDrives()
# This function determines the total number of drives installed in the system
# and returns the count of HDD and SSD drives.
##################################################################################
def countDrives():
	ssd_count = 0
	hdd_count = 0
	devices = []
	try:
		ls_result = subprocess.Popen(
			["ls","-w","1","/sys/block/"],stdout=subprocess.PIPE,universal_newlines=True)
	except:
		print("Error Counting Drives")
		exit(1)
	for line in ls_result.stdout:
		regex_drive_count = re.search("^(sd.*)$",line)
		if regex_drive_count != None:
			devices.append(regex_drive_count.group(1))
	
	for dev in devices:
		rotational_path = "/sys/block/" + dev + "/queue/rotational"
		if os.path.isfile(rotational_path):
			rotational = open(rotational_path, mode='r')
			if bool(int(rotational.read(1))):
				hdd_count += 1
			else:
				ssd_count += 1
	return hdd_count, ssd_count

##################################################################################
# getChassisType()
# gets the chassis type based on user input. returns a string. 
##################################################################################
def getChassisType():
	done = False
	step_number = sn=updateProgressStep()
	while not done:
		print("\n")
		print("+----------------------------------------------------------------+")
		print("| Step {sn}: Select a Chassis Type                                  |".format(sn=step_number))
		print("+----------------------------------------------------------------+")
		option = 0
		for product in g_config_file_content["Chassis Type"]:
			option += 1
			print("\t",option,": " + product)
		selection = input("Select an option (1 - " + str(option) + "): ")
		if selection.isnumeric() and int(selection) <= option and int(selection) > 0:
			return g_config_file_content["Chassis Type"][int(selection)-1]
		else:
			print("Invalid selection")

##################################################################################
# getProductType()
# Gets the product type selection from the user. Returns a string.
##################################################################################
def getProductType():
	done = False
	step_number = updateProgressStep()
	while not done:
		print("+----------------------------------------------------------------+")
		print("| Step {sn}: Select Product Type                                    |".format(sn=step_number))
		print("+----------------------------------------------------------------+")
		option = 0
		for product in g_config_file_content["Product Type"]:
			option += 1
			print("\t",option,": " + product)
		selection = input("Select an option (1 - " + str(option) + "): ")
		if selection.isnumeric() and int(selection) <= option and int(selection) > 0:
			return g_config_file_content["Product Type"][int(selection)-1]
		else:
			print("Invalid selection")

##################################################################################
# getSerial()
# gets the serial number from the user. Performs basic check to ensure that
# numbers and hyphens are used. Typical job number is of format XXXXXXXX-YY-ZZ
##################################################################################
def getSerial():
	done = False
	selection = ""
	step_number = updateProgressStep()
	while not done:
		print("\n")
		print("+----------------------------------------------------------------+")
		print("| Step {sn}: Enter Product Serial                                   |".format(sn=step_number))
		print("+----------------------------------------------------------------+")
		selection = input("Enter Product Serial Number (aka Job Number ex:104237-11-01): ")
		selCopy = selection
		selCopy = selCopy.split("-")
		if len(selCopy) == 3:
			#serial is of proper length
			if selCopy[0].isnumeric() and selCopy[2].isnumeric():
				# the first and last tokens are of proper format
				done = True
		elif len(selCopy) == 2:
			#serial is of improper length
			if selCopy[0].isnumeric():
				print("Serial number entered ("+selection+") doesn't match a full serial number format (ex. 123456-10-01).")
			if input("Are you sure that you want to use an incomplete serial number for this system? It can make support's job harder if you do.. (y/n):") == "y":
				done = True
		else:
			print("Invalid format used, try again")
	return selection


##################################################################################
# getHybridType()
# Lets the user specify if the product is a hybrid or not. 
##################################################################################
def getHybridType():
	done = False
	step_number = updateProgressStep()
	while not done:
		print("\n")
		print("+----------------------------------------------------------------+")
		print("| Step {sn}: Hybrid Check                                           |".format(sn=step_number))
		print("+----------------------------------------------------------------+")

		valid_hybrid_strs = ["H32","H16","H8"]
		hybrid_str = ""
		# prompt user to see if system is a hybrid system.
		hybrid = input("Is this a hybrid machine? (y/n): ")
		if hybrid == "y":
			# we have a hybrid system, append an H to string
			hybrid_str += "H"
			# get type of hybrid from user.
			hybridCount = input("which kind of hybrid? (8/16/32): ")
			hybrid_str += hybridCount
			if hybrid_str in valid_hybrid_strs:
				return hybrid_str
			else:
				print("Invalid hybrid type \"{h}\". Try Again.".format(h=hybrid_str))
		elif hybrid == "n":
			return ""
		else:
			print("Invalid Selection")


##################################################################################
# getUserInput()
# Function that calls all user input functions outside of autoDetect().
# updates auto_detect_fields and fru_fields (declared in main()).
##################################################################################
def getUserInput(auto_detect_fields,fru_fields):
	product_type = getProductType()
	hybrid_type = getHybridType()
	chassis_type = getChassisType()
	serial = getSerial()
	asset_tag = "SELFSERIALIZED"

	auto_detect_fields["product_type"] = product_type
	auto_detect_fields["part_number"] = chassis_type
	auto_detect_fields["hybrid_type"] = hybrid_type
	fru_fields["part_number"] = chassis_type
	fru_fields["serial"] = serial
	fru_fields["asset_tag"] = asset_tag
	fru_fields["product_name"] = product_type

def assignProductName(auto_detect_fields,fru_fields):
	mobo_to_version_lut = {
		"X11SSH-CTF":"Base",
		"X11SSM-F": "Base",
		"X11DPL-i": "Turbo",
		"X11DPH-T": "Turbo",
		"X10DRL-i": "Turbo"
	}

	cpu_substr_to_version_lut = {
		"Bronze":"Base-B",
		"Silver":"Enhanced-S",
		"Gold":"Turbo-G",
		"AMD":"Enhanced-AMD"
	}

	step_number = updateProgressStep()
	while True:
		print("\n")
		print("+----------------------------------------------------------------+")
		print("| Step {sn}: Determine Unique Server Type                           |".format(sn=step_number))
		print("+----------------------------------------------------------------+")
		
		final_product_str = "unknown"
		unique_product_str_tmp = None

		# build the product string using the input provided by the user.
		unique_product_str = auto_detect_fields["product_type"] + "-"

		if auto_detect_fields["hybrid_type"] != "":
			unique_product_str += auto_detect_fields["hybrid_type"] + "-"

		unique_product_str += auto_detect_fields["part_number"] + "-"

		#see if we can determine base, enhanced, turbo or enhanced-AMD type
		if auto_detect_fields["board_model"] in mobo_to_version_lut.keys():
			unique_product_str_tmp = unique_product_str + mobo_to_version_lut[auto_detect_fields["board_model"]]

		else:
			# determine model based on cpu present
			cpu_substr_present = False
			cpu_substr_version = None

			for substr in cpu_substr_to_version_lut.keys():
				if substr in auto_detect_fields["cpu_model"] and not cpu_substr_present:
					cpu_substr_present = True
					cpu_substr_version = cpu_substr_to_version_lut[substr]

			if cpu_substr_present:
				unique_product_str_tmp = unique_product_str + cpu_substr_version

		if unique_product_str_tmp in g_config_file_content["Product Name"]:
			final_product_str = unique_product_str_tmp
		
		if final_product_str != "unknown":
			# we got valid input from user and assigned a value to final product string.
			auto_detect_fields["unique_product_str"] = final_product_str
			confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n):".format(pr=auto_detect_fields["unique_product_str"]))
			if confirm == "y":
				# user confirmed final product string to be correct. We're all done.
				fru_fields["product_name"] = auto_detect_fields["unique_product_str"]
				fru_fields["version"] = auto_detect_fields["unique_product_str"].split("-")[-1]
				return
			else:
				final_product_str = "unknown"
			
		# Unable to automatically determine product version, get 
		# this information from user. 
		versions = ["Base","Base-B","Enhanced","Enhanced-S","Turbo","Turbo-G","Enhanced-AMD"]
		print("Select a system version:")
		option = 0
		for version in versions:
			option += 1
			print("\t" + str(option) + ": " + version)
		selection = input("Select an option (1 - " + str(option) + "): ")
		if selection.isnumeric() and int(selection) <= option and int(selection) > 0:
			unique_product_str += versions[int(selection)-1]
			if unique_product_str in g_config_file_content["Product Name"]:
				final_product_str = unique_product_str
			else:
				print("Product Configuration of \"{ps}\" is INVALID. \nRun Program again or contact 45Drives Support if issue persists.".format(ps=unique_product_str))
				print("Valid Product Configurations: ")
				for valid_product in g_config_file_content["Product Name"]:
					print("\t{vp}".format(vp=valid_product))
				input("Press ENTER to End Program.")
				exit(0)

		if final_product_str != "unknown":
			# we got valid input from user and assigned a value to final product string.
			auto_detect_fields["unique_product_str"] = final_product_str
			confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n):".format(pr=auto_detect_fields["unique_product_str"]))
			if confirm == "y":
				# user confirmed final product string to be correct. We're all done.
				fru_fields["product_name"] = auto_detect_fields["unique_product_str"]
				fru_fields["version"] = auto_detect_fields["unique_product_str"].split("-")[-1]
				return

		# We need to try to get user unput again. Something was entered incorrectly. 
		print("Invalid information entered, Trying Again.\n")


##################################################################################
#  get_ipmitool_information(product_serial):
#  Obtains parameters required to create the fru.ini config file that
#  is then used to generate the binary fru data. It also stores a backup of 
#  the existing fru data as a binary file. 
##################################################################################
def get_ipmitool_information(product_serial):
	ipmitool_fru_output = {}

	# Get current FRU fields in plain text
	ipmitool_fru_result = None
	try:
		ipmitool_fru_result = subprocess.Popen(["ipmitool", "fru"],stdout=subprocess.PIPE,universal_newlines=True)
	except:
		print("Error executing \"ipmitool fru\"")
		exit(1)

	# read in all key value pairs from ipmitool fru plain text output.
	key_value_fru_regex = r"\s([\sA-Za-z]+)(?:\s+):(?:\s+)([^\n]+)"
	for line in ipmitool_fru_result.stdout:
		match = re.search(key_value_fru_regex, line)

		if match != None:
			key = match.group(1).strip()
			value = match.group(2).strip()
			ipmitool_fru_output[key] = value

	# create a backup directory for the existing fru binary data.
	backup_dir = f"{g_current_dir}/fru_bin_backups"
	if not os.path.exists(backup_dir):
		print(f"making directory to store fru bin backups in {backup_dir}")
		os.makedirs(backup_dir)
	# create filename for existing fru binary data
	date = datetime.datetime.now()
	timestamp = date.strftime("%Y-%m-%d_%H-%M-%S")
	backup_name = f"{backup_dir}/fru-{product_serial}-{timestamp}.bin".replace(" ", "")

	# read existing fru binary data from motherboard.
	ipmitool_fru_read_result = None
	try:
		ipmitool_fru_read_result = subprocess.Popen(["ipmitool", "fru", "read", "0", backup_name],stdout=subprocess.PIPE,universal_newlines=True).stdout.read()
	except:
		print("Error executing \"ipmitool fru read\"")
		exit(1)

	# capture the size of the existing fru data (bytes)
	match = re.search(r"(?:fru\ssize\s+):\s+(\d+)\s+([^\n]+)", ipmitool_fru_read_result, flags=re.IGNORECASE)
	if match != None:
		ipmitool_fru_output["FRU Size"] = match.group(1)

	# get minutes since Jan 1st 1996
	month_lut = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]
	start_date = datetime.datetime(1996, 1, 1, 0, 0).timestamp()
	end_date = None

	# get board manufacturing date if present
	if "Board Mfg Date" in ipmitool_fru_output:
		date_match = re.search(r"(?:[A-Za-z]{3})\s+([A-Za-z]{3})\s+([0-9]{1,2})\s+([0-9]{1,2}):([0-9]{1,2}):(?:[0-9]{1,2})\s+([0-9]{4})", ipmitool_fru_output["Board Mfg Date"])

		if date_match != None:
			month = month_lut.index(date_match.group(1).lower()) + 1
			day = int(date_match.group(2))
			year = int(date_match.group(5))
			hour = int(date_match.group(3))
			minute = int(date_match.group(3))

			end_date = datetime.datetime(year, month, day, hour, minute).timestamp()

	if end_date == None:
		# use current date/time as fallback in case board manufacturing date is not present/invalid
		end_date = datetime.datetime.now().timestamp()

	# convert to required hex value for flashing.
	minutes = int((end_date - start_date) / 60)
	minutes_hex = hex(minutes).lstrip("0x")

	# return board manufacture date in hex, fru size (bytes), and other plaintext values required for .ini file.
	ipmitool_fru_output["Board Mfg Date Hex"] = minutes_hex.upper()
	return ipmitool_fru_output


##################################################################################
# storePrevIpmitoolOutput():
# Automatically stores a timestamped plaintext output of ipmitool fru. 
# in case we want a reference of previous fru data in human-readable format. 
##################################################################################
def storePrevIpmitoolOutput():
	print("\n")
	print("+----------------------------------------------------------------+")
	print("| Step {sn}: Store backup                                           |".format(sn=updateProgressStep()))
	print("+----------------------------------------------------------------+")
	print("\nStoring plaintext output of 'ipmitool fru' command in backup file before writing.")
	try:
		ipmitool_result = subprocess.Popen(
			["ipmitool","fru"],stdout=subprocess.PIPE,universal_newlines=True)
	except:
		print("Error executing \"ipmitool fru\"")
		exit(1)	

	ipmitool_fru_output = []
	for line in ipmitool_result.stdout:
		ipmitool_fru_output.append(line)

	backup_dir = f"{g_current_dir}/fru_plaintext_backups"
	if not os.path.exists(backup_dir):
		print("making directory to store backup files {bd}".format(bd=backup_dir))
		os.makedirs(backup_dir)

	date = datetime.datetime.now()
	timestamp = date.strftime("%Y-%m-%d_%H-%M-%S")
	backup_file_name = "{bd}/ipmitool_fru_backup_{ts}".format(bd=backup_dir,ts=timestamp)

	b_file = open(backup_file_name,"w")
	for line in ipmitool_fru_output:
		b_file.write(line)
	b_file.write("\n")
	b_file.close()
	print("Output stored successfully ({bfn})".format(bfn=backup_file_name))

def check_root():
	root_test = subprocess.run(["ls","/root"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode
	if root_test:
		print("serial45d must be run with root privileges.")
		exit(root_test)

def updateProgressStep(inc=True,stepValue=1):
	global g_progress_step
	if inc:
		g_progress_step = g_progress_step + 1
	else:
		g_progress_step = stepValue
	return g_progress_step



##################################################################################
# verifyIpmitoolFru():
# Checks to ensure that all required fields are present from the command "ipmitool fru".
# if they are not present it will attempt to obtain it from demidecode. 
# if all else fails it will prompt the user for the missing information.
##################################################################################
def verifyIpmitoolFru(ipmitool_information):
	print("\n")
	print("+----------------------------------------------------------------+")
	print("| Step {sn}: Verify FRU Data                                        |".format(sn=updateProgressStep()))
	print("+----------------------------------------------------------------+")
	print("\nValidating information obtained from 'ipmitool fru' command.")


	required_fields = ["Board Product","Board Serial","Board Mfg"]
	valid = True

	# look through the required fields and flag any missing or empty fields as invalid
	for field in required_fields:
		if field not in ipmitool_information.keys():
			valid = False
			print("\tMissing Field: '{k}'".format(k=field))
		elif not ipmitool_information[field]:
			valid = False
			print("\tEmpty Field: '{f}': '{v}'".format(f=field,v=ipmitool_information[field]))
	
	if valid:
		print("FRU data is VALID.")
		return

	print("\tFRU data is invalid, attempting to obtain motherboard data from dmidecode.")	
	board_product, board_serial, board_mfg = getMotherboard()
	ipmitool_information["Board Product"] = board_product if board_product != "unknown" else ""
	ipmitool_information["Board Serial"] = board_serial if board_serial != "unknown" else ""
	ipmitool_information["Board Mfg"] = board_mfg if board_mfg != "unknown" else ""

	# re-test fields after obtaining them from dmidecode
	valid = True

	# look through the required fields and flag any missing or empty fields as invalid
	for field in required_fields:
		if field not in ipmitool_information.keys():
			valid = False
			print("\tMissing Field: '{k}'".format(k=field))
		elif not ipmitool_information[field]:
			valid = False
			print("\tEmpty Field: '{f}': '{v}'".format(f=field,v=ipmitool_information[field]))
	
	if valid:
		print("\tValid FRU Data obtained from 'dmidecode -t 2'.")
		return
	
	# neither ipmitool fru or dmidecode worked to automatically detect the board information. 
	# Last resort is to prompt the user to input this information instead.
	global g_config_file_content

	# Get Motherboard Manufacturer
	if not ipmitool_information.get("Board Mfg",""):
		valid_selection = ["Supermicro","ASRockRack"]
		done = False
		step_number = updateProgressStep()
		while not done:
			print("+----------------------------------------------------------------+")
			print("| Step {sn}: Select Motherboard Manufacturer                        |".format(sn=step_number))
			print("+----------------------------------------------------------------+")
			option = 0
			for product in valid_selection:
				option += 1
				print("\t",option,": " + product)
			selection = input("Select an option (1 - " + str(option) + "): ")
			if selection.isnumeric() and int(selection) <= option and int(selection) > 0:
				ipmitool_information["Board Mfg"] = valid_selection[int(selection)-1]
				done = True
			else:
				print("Invalid selection")

	# Get Motherboard Model
	if not ipmitool_information.get("Board Product",""):
		valid_selection = g_config_file_content.get("Motherboard",["UNKNOWN"])
		done = False
		step_number = updateProgressStep()
		while not done:
			print("+----------------------------------------------------------------+")
			print("| Step {sn}: Select Motherboard Model                               |".format(sn=step_number))
			print("+----------------------------------------------------------------+")
			option = 0
			for product in valid_selection:
				option += 1
				print("\t",option,": " + product)
			selection = input("Select an option (1 - " + str(option) + "): ")
			if selection.isnumeric() and int(selection) <= option and int(selection) > 0:
				ipmitool_information["Board Product"] = valid_selection[int(selection)-1]
				done = True
			else:
				print("Invalid selection")

	# Get Serial Number
	if not ipmitool_information.get("Board Serial",""):
		done = False
		selection = ""
		step_number = updateProgressStep()
		while not done:
			print("\n")
			print("+----------------------------------------------------------------+")
			print("| Step {sn}: Enter Motherboard Serial Number                        |".format(sn=step_number))
			print("+----------------------------------------------------------------+")
			selection = input("\tEnter Motherboard Serial Number (ex:ZM228S003796): ")
			confirm = input("\tSerial Number Entered: '{sn}' Use this serial number? (y/n): ".format(sn=selection))
			if confirm == "y":
				done = True
				ipmitool_information["Board Serial"] = selection.strip()

def main():
	check_root()

	# Fields used in the IPMICFG-Linux.x86_64 write operation
	fru_fields = {
		"manufacturer_name":"45Drives",
		"product_name":"unknown",
		"part_number":"unknown",
		"version":"unknown",
		"serial":"unknown",
		"asset_tag":"unknown",
		"board_manufacturing_date":"unknown",
		"board_manufacturer":"unknown",
		"board_model":"unknown",
		"board_serial":"unknown",
		"chassis_type":"unknown",
		"chassis_part_number":"unknown",
		"chassis_serial":"unknown"
	}

	# Fields used to attempt to automatically detect the product
	auto_detect_fields = {
		"product_type":"unknown",
		"board_model":"unknown",
		"cpu_model":"unknown",
		"cpu_count":0,
		"chassis_type":"unknown",
		"unique_product_str":"unknown",
		"hybrid_type": ""
	}

	outputSerialVersion()
	
	# load config file information and store it in g_config_file_content
	loadSystemConfigs()

	# get system type, chassis size, and serial number from user.
	getUserInput(auto_detect_fields,fru_fields)

	ipmitool_information = get_ipmitool_information(fru_fields["serial"])
	verifyIpmitoolFru(ipmitool_information)

	if "Product Asset Tag" in ipmitool_information:
		fru_fields["asset_tag"] = ipmitool_information["Product Asset Tag"]

		if not fru_fields["asset_tag"].startswith('SS-'):
			fru_fields["asset_tag"] = f'SS-{fru_fields["asset_tag"]}'

	#update automatically detected fru fields
	fru_fields["board_model"] = ipmitool_information["Board Product"]
	fru_fields["board_serial"] = ipmitool_information["Board Serial"]
	fru_fields["board_manufacturer"] = ipmitool_information["Board Mfg"]
	auto_detect_fields["board_model"] = ipmitool_information["Board Product"]

	# gather information and perform autodetection
	auto_detect_fields["cpu_model"], auto_detect_fields["cpu_count"] = getCPU()
	assignProductName(auto_detect_fields,fru_fields)
	storePrevIpmitoolOutput()

	# Keeping this in case we run into issues writing to supermicro boards.
	#if "supermicro" in fru_fields["board_manufacturer"].lower():
	#	performFRUWriteSuperMicro(fru_fields)

	# write fru binary data, using ipmitool or supermicro restore utility.
	performFRUWrite(ipmitool_information,fru_fields)

if __name__ == "__main__":
	main()