#!/usr/bin/python3

################################################################################
# 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
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
from optparse import OptionParser

g_ps_serial = None

g_production_mode = False

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

ANSI_colors={
	"LGREEN":'\033[1;32m',
	"GREEN":'\033[0;32m',
	"YELLOW":'\033[0;33m',
	"MAGENTA":'\033[0;35m',
	"CYAN":'\033[0;36m',
	"WHITE":'\033[0;37m',
	"RED":'\033[0;31m',
	"GREY":'\033[1;30m',
	"END":'\033[0m'
}

def outputSerialVersion():
    global g_production_mode
    print("\n-----------------------------------------------------------------------------------")
    if g_production_mode:
        print("{g}                     *** 45Drives PRODUCTION MODE ***              {e}".format(g=ANSI_colors["GREEN"],e=ANSI_colors["END"]))
    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))
        if g_production_mode:
            print("{g}                     *** 45Drives PRODUCTION MODE ***              {e}".format(g=ANSI_colors["GREEN"],e=ANSI_colors["END"]))    
        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,timestamp):
    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': '',
        },
    }

    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)	

    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_fru_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

##################################################################################
# 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")

def checkExistingSerial():
    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
    
    if "Chassis Serial" in ipmitool_fru_output.keys() and checkSerialFormat(ipmitool_fru_output["Chassis Serial"]):
        return ipmitool_fru_output["Chassis Serial"]
    elif "Product Serial" in ipmitool_fru_output.keys() and checkSerialFormat(ipmitool_fru_output["Product Serial"]):
        return ipmitool_fru_output["Product Serial"]
    else:
        return ""

def checkSerialFormat(serial_str):
    serial_format_regex = r"(\d+-\w+-?\w+)"
    match = re.search(serial_format_regex,serial_str)
    if match != None:
        return True
    else:
        return False

##################################################################################
# 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():
    global g_production_mode
    done = False
    selection = ""
    step_number = updateProgressStep()
    if not g_production_mode:
        selection = checkExistingSerial()
        if selection != "":
            print("\n")
            print("+----------------------------------------------------------------+")
            print("| Step {sn}: Enter Product Serial                                   |".format(sn=step_number))
            print("+----------------------------------------------------------------+")
            print("45Drives Serial Number '{sn}' detected automatically.".format(sn=selection))
            resp = input("Change serial number? (y/N): ")
            done = not re.match(r'^[Yy][Ee]?[Ss]?$', resp.strip())
        
    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 45Drives serial number format (ex. 123456-10-01).")
            
            if not g_production_mode:
                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
            elif 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":
                print("Well that's not very nice..")
                if input("Want me to add a '-01' on the end of '{sel}' for you? (y/n): ".format(sel=selection)) == "y":
                    selection = selection + "-01"
                    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 Gen 1 Hybrid Server? note: F8X servers are not Gen 1 Hybrid Servers. (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")

def enforce_homelab_blocks(fru_fields):
    """
    Block serialization paths that must use the 'serial.bat' flow.

    Rules:
      - HL4 and HL8: always block here (must use serial.bat).
      - HL15: only block when it's the HL15_BEAST variant AND the motherboard
        is ProArt X870E-CREATOR WIFI.
    """
    product_name = (fru_fields.get("product_name") or "").strip()
    board_model = (fru_fields.get("board_model") or "").strip()

    # Normalize for safe comparisons
    pn = product_name
    bm = board_model.lower()

    def _blocked(reason_lines):
        print("\n")
        print("+----------------------------------------------------------------+")
        print("|                          OPERATION BLOCKED                     |")
        print("+----------------------------------------------------------------+")
        for l in reason_lines:
            print(l)
        print("")
        print("This configuration must be serialized using the serial.bat script from a bootable USB.")
        print("")
        exit(2)

    # Always block HL4 / HL8
    if pn in ("HomeLab-HL4", "HomeLab-HL8"):
        _blocked([
            f"Detected product: {pn}",
            "Policy: HomeLab HL4/HL8 units are serialized via serial.bat script."
        ])

    # For HL15, only block the BEAST variant with this specific board
    if pn == "HomeLab-HL15_BEAST" and bm == "proart x870e-creator wifi":
        _blocked([
            "Detected product: HomeLab-HL15_BEAST",
            "Detected motherboard: ProArt X870E-CREATOR WIFI",
            "Policy: HL15_BEAST + ProArt board requires serial.bat script serialization."
        ])

    # Otherwise: allowed to proceed

def maybe_fail_fast_after_chassis(auto_detect_fields, fru_fields):
    """
    Fail fast for HomeLab SKUs immediately after chassis selection, before asking for serial.
    Uses dmidecode to grab the board model when needed (HL15_BEAST rule).
    """
    # Tentative product_name guess purely from the user's selections
    pn = None
    pt = auto_detect_fields.get("product_type", "") or ""
    ch = auto_detect_fields.get("part_number", "") or ""

    if "HomeLab" in pt:
        if "HL4" in ch:
            pn = "HomeLab-HL4"
        elif "HL8" in ch:
            pn = "HomeLab-HL8"
        elif "HL15_BEAST" in ch:
            pn = "HomeLab-HL15_BEAST"
        # Plain HL15 is allowed; don't set pn for that case.

    if pn:
        fru_fields["product_name"] = pn

        # For HL15_BEAST we need the board model for the conditional rule.
        # For HL4/HL8 it's a straight block with no board dependency,
        # but grabbing the board here is cheap and harmless.
        board_model, board_serial, board_mfg = getMotherboard()
        fru_fields["board_model"] = board_model
        fru_fields["board_manufacturer"] = board_mfg

        # This will exit(2) if blocked.
        enforce_homelab_blocks(fru_fields)


##################################################################################
# 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):
    global g_production_mode
    global g_ps_serial
    product_type = getProductType()
    hybrid_type = getHybridType()
    chassis_type = getChassisType()

    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["product_name"] = product_type

    maybe_fail_fast_after_chassis(auto_detect_fields, fru_fields)

    serial = getSerial()
    asset_tag = g_ps_serial.getAssetTag(updateProgressStep()) if g_production_mode else "SELFSERIALIZED"
    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

        if "HomeLab" in auto_detect_fields["product_type"]:
            if "HL4" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="HomeLab-HL4"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "HomeLab-HL4"
                    fru_fields["version"] = "HomeLab"
                    auto_detect_fields["unique_product_str"] = "HomeLab-HL4"

                    # Enforce serial.bat-only serialization for HL4
                    enforce_homelab_blocks(fru_fields)
                    return

            if "HL8" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="HomeLab-HL8"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "HomeLab-HL8"
                    fru_fields["version"] = "HomeLab"
                    auto_detect_fields["unique_product_str"] = "HomeLab-HL8"

                    # Enforce serial.bat-only serialization for HL8
                    enforce_homelab_blocks(fru_fields)
                    return

            if "HL15_BEAST" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="HomeLab-HL15_BEAST"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "HomeLab-HL15_BEAST"
                    fru_fields["version"] = "HomeLab"
                    auto_detect_fields["unique_product_str"] = "HomeLab-HL15_BEAST"

                    # Enforce serial.bat conditional block (only for ProArt X870E-CREATOR WIFI)
                    enforce_homelab_blocks(fru_fields)
                    return
            elif "HL15" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="HomeLab-HL15"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "HomeLab-HL15"
                    fru_fields["version"] = "HomeLab"
                    auto_detect_fields["unique_product_str"] = "HomeLab-HL15"
                    return

            if "X15" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="HomeLab-X15"))
                if confirm == "y":
                    fru_fields["product_name"] = "HomeLab-X15"
                    fru_fields["version"] = "HomeLab"
                    auto_detect_fields["unique_product_str"] = "HomeLab-X15"
                    return
        
        if "Professional" in auto_detect_fields["product_type"]:
            if "PRO4" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="Professional-PRO4"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "Professional-PRO4"
                    fru_fields["version"] = "Professional"
                    auto_detect_fields["unique_product_str"] = "Professional-PRO4"
                    return
            if "PRO8" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="Professional-PRO8"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "Professional-PRO8"
                    fru_fields["version"] = "Professional"
                    auto_detect_fields["unique_product_str"] = "Professional-PRO8"
                    return
            if "PRO15" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="Professional-PRO15"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "Professional-PRO15"
                    fru_fields["version"] = "Professional"
                    auto_detect_fields["unique_product_str"] = "Professional-PRO15"
                    return
        if "Studio" in auto_detect_fields["product_type"]:
            if "STUDIO8" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="Studio-STUDIO8"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "Studio-STUDIO8"
                    fru_fields["version"] = "Studio"
                    auto_detect_fields["unique_product_str"] = "Studio-STUDIO8"
                    return
            if "STUDIO15" in auto_detect_fields["part_number"]:
                confirm = input("\nAuto-Detect determined that the product is a \"{pr}\", is this correct? (y/n): ".format(pr="Studio-STUDIO15"))
                if confirm == "y":
                    # user confirmed final product string to be correct. We're all done.
                    fru_fields["product_name"] = "Studio-STUDIO15"
                    fru_fields["version"] = "Studio"
                    auto_detect_fields["unique_product_str"] = "Studio-STUDIO15"
                    return


        # 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"]]
            # print("[DEBUG] 1:", unique_product_str_tmp)

        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
                # print("[DEBUG] 2:", unique_product_str_tmp)

        # print("[DEBUG] 3:", unique_product_str_tmp)
        if unique_product_str_tmp in g_config_file_content["Product Name"]:
            final_product_str = unique_product_str_tmp
        
        # print("[DEBUG] 4:", final_product_str)
        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"]
        versions = []
        for product in g_config_file_content["Product Name"]:
            if unique_product_str in product:
                versions.append(product.split(unique_product_str)[1])
        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,timestamp):
    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
    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)
    
    print("\n")
    print("+----------------------------------------------------------------+")
    print("| Step {sn}: Store backup (binary data)                             |".format(sn=updateProgressStep()))
    print("+----------------------------------------------------------------+")
    print(f"Storing backup of current fru data: {backup_name}")

    # 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(timestamp):
    print("\n")
    print("+----------------------------------------------------------------+")
    print("| Step {sn}: Store backup (plaintext)                                |".format(sn=updateProgressStep()))
    print("+----------------------------------------------------------------+")
    print("Storing 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)

    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):
    global g_production_mode
    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
    
    if not g_production_mode:
        # 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("\tAttempting 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 load_production_module():
    global g_production_mode
    if os.path.exists("/opt/production-scripts/serial/serial_production"):
        spec = spec_from_loader("serial", SourceFileLoader(
        "serial", "/opt/production-scripts/serial/serial_production"))
        ps_serial = module_from_spec(spec)
        spec.loader.exec_module(ps_serial)
        g_production_mode = True
        return ps_serial
    return None

def clear_fru_data(ipmitool_information,timestamp):
    print("\n")
    print("+----------------------------------------------------------------+")
    print("| Step {sn}: Clear FRU Data                                         |".format(sn=updateProgressStep()))
    print("+----------------------------------------------------------------+")
    print("Clearing FRU Data")
    performFRUClear(ipmitool_information,timestamp)


def performFRUClear(ipmitool_information,timestamp):
    fru_structure = {
        'common': {
            'version': 1,
            'size': ipmitool_information.get("FRU Size","256"),
            'internal': 0,
            'chassis': 0,
            'board': 1,
            'product': 0,
            'multirecord': 0,
        },
        '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",""),
            'fileid': '',
        }
    }

    # 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-clear-{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-clear-{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 ipmitool_information.get("Board Mfg","").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)	

    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_fru_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

def fru_restore(timestamp):
    print("\n")
    print("+----------------------------------------------------------------+")
    print("| Step {sn}: Select a backup file                                   |".format(sn=updateProgressStep()))
    print("+----------------------------------------------------------------+")
    print("Select a backup to restore from: ")
    backup_file_dir = "/opt/45drives/serial45d/fru_bin_backups"
    backups = os.listdir(backup_file_dir)
    backup_file_name = ""
    done = False

    while not done:
        option = 0
        for backup in backups:
            option += 1
            print("\t",option,": " + backup)
        selection = input("Select an option (1 - " + str(option) + "): ")
        if selection.isnumeric() and int(selection) <= option and int(selection) > 0:
            done = True
            backup_file_name = backups[int(selection)-1]
        else:
            print("Invalid selection")

    backup_file_path = f"{backup_file_dir}/{backup_file_name}"
    
    pt_backup_dir = "/opt/45drives/serial45d/fru_plaintext_backups"
    pt_backups = os.listdir(pt_backup_dir)
    pt_fn_prefix = "ipmitool_fru_backup_"

    for pt in pt_backups:
        if pt[len(pt_fn_prefix):] in backup_file_name:
            pt_backup_path = f"{pt_backup_dir}/{pt}"
            with open(pt_backup_path,"r") as f:
                print(f"\nPreview from {pt_backup_path}: ")
                print(f.read())
            break
    confirm = input("Restore fru data from '{bfp}'? (y/n): ".format(bfp=backup_file_path))
    if confirm not in ["y","Y"]:
        print("exiting..")
        exit(0)

    # get existing information and create a backup in both binary and plaintext.
    ipmitool_information = get_ipmitool_information("restore",timestamp)
    storePrevIpmitoolOutput(timestamp)
    
    arg_vector = ["ipmitool", "fru", "write", "0", backup_file_path]

    if "supermicro" in ipmitool_information.get("Board Mfg","").lower():
        arg_vector = [
            f"{g_current_dir}/dependencies/ipmi_linux/IPMICFG-Linux.x86_64",
            "-fru",
            "restore",
            backup_file_path
        ]
            
    # Write the backup file contents.
    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)	

    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_fru_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


def main():
    global g_ps_serial
    global g_production_mode
    date = datetime.datetime.now()
    timestamp = date.strftime("%Y-%m-%d_%H-%M-%S")
    check_root()

    # get command line arguments
    parser = OptionParser()
    parser.add_option("-c","--clear",action="store_true",dest="clear_fru",default=False,help="Clear FRU Fields on Motherboard")
    parser.add_option("-r","--restore",action="store_true",dest="restore",default=None,help="Restore FRU data from backup file in /opt/45drives/serial45d/fru_bin_backups/")
    parser.add_option("--no-prod", action="store_true", dest="no_prod", default=False,
                  help="Ignore production overlay module even if present")
    (options, args) = parser.parse_args()

    
    if options.clear_fru:
        outputSerialVersion()
        ipmitool_information = get_ipmitool_information("clear",timestamp)
        storePrevIpmitoolOutput(timestamp)
        clear_fru_data(ipmitool_information,timestamp)
        exit(0)

    if options.restore:
        outputSerialVersion()
        fru_restore(timestamp)
        exit(0)
    
    # g_ps_serial = load_production_module()
    if options.no_prod or os.environ.get("SERIAL45D_NO_PROD") == "1":
        g_ps_serial = None
        g_production_mode = False
    else:
        g_ps_serial = load_production_module()
    
    # 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"],timestamp)
    verifyIpmitoolFru(ipmitool_information)

    if not g_production_mode:
        if "Product Asset Tag" in ipmitool_information.keys() and "SELFSERIALIZED" not in ipmitool_information["Product Asset Tag"] and "N/A" not in ipmitool_information["Product Asset Tag"]:
            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)
    
    # Safety net: block disallowed HomeLab paths even if product_name was set elsewhere
    enforce_homelab_blocks(fru_fields)

    storePrevIpmitoolOutput(timestamp)

    # 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.
    fru_write_output = performFRUWrite(ipmitool_information,fru_fields,timestamp)

    if g_production_mode:
        g_ps_serial.setFanMode(fru_fields)

if __name__ == "__main__":
    main()
