#!/usr/libexec/platform-python
########################################################################################
# server_identifier
# This script can be run on standard 45Drives storage servers to determine the system 
# model by gathering hardware infromation.
########################################################################################
import subprocess
import re
import json
import os.path
import os
from datetime import datetime
import sys

g_product_lut_idx = {
	"MOBO_MODEL":	0,
	"24I_COUNT":	1,
	"16I_COUNT":	2,
	"CHASSIS_SIZE": 3,
	"ALIAS_STYLE":	4
}

g_chassis_sizes = ["?","AV15","Q30","S45","XL60"]

g_product_lut = {
	"Storinator-AV15-Base":				["X11SSH-CTF",0,0,"AV15","AV15-BASE"],
	"Storinator-AV15-Base-B":			["X11SPL-F"  ,0,1,"AV15","STORINATOR"],
	"Storinator-AV15-Enhanced":			["X11SPL-F"  ,0,1,"AV15","STORINATOR"],
	"Storinator-AV15-Enhanced-S":		["X11SPL-F"  ,0,1,"AV15","STORINATOR"],
	"Storinator-AV15-Enhanced-AMD":		["H11SSL-i"  ,0,1,"AV15","STORINATOR"],
	"Storinator-AV15-Turbo":			["X11DPL-i"  ,0,1,"AV15","STORINATOR"],
	"Storinator-AV15-Turbo-G":		    ["X11SPL-F"  ,0,1,"AV15","STORINATOR"],

	"Storinator-C8-Base":				["X11SSM-F"  ,0,1,"C8"  ,"STORINATOR"],
	"Storinator-C8-Base-B":				["X11SPL-F"  ,0,1,"C8"  ,"STORINATOR"],
	"Storinator-C8-Enhanced":			["X11SPL-F"  ,0,1,"C8"  ,"STORINATOR"],
	"Storinator-C8-Enhanced-S":			["X11SPL-F"  ,0,1,"C8"  ,"STORINATOR"],
	"Storinator-C8-Enhanced-AMD":		["H11SSL-i"  ,0,1,"C8"  ,"STORINATOR"],
	"Storinator-C8-Turbo":				["X11DPL-i"  ,0,1,"C8"  ,"STORINATOR"],
	"Storinator-C8-Turbo-G":			["X11SPL-F"  ,0,1,"C8"  ,"STORINATOR"],
	
	"Storinator-H8-AV15-Base-B":		["X11SPL-F"  ,0,1,"AV15","STORINATOR"],
	"Storinator-H8-AV15-Enhanced":		["X11SPL-F"  ,0,1,"AV15","STORINATOR"],
	"Storinator-H8-AV15-Enhanced-S":	["X11SPL-F"  ,0,1,"AV15","STORINATOR"],
	"Storinator-H8-AV15-Enhanced-AMD":	["H11SSL-i"  ,0,1,"AV15","STORINATOR"],
	"Storinator-H8-AV15-Turbo":			["X11DPL-i"  ,0,1,"AV15","STORINATOR"],
	"Storinator-H8-AV15-Turbo-G":		["X11SPL-F"  ,0,1,"AV15","STORINATOR"],

	"Storinator-H8-Q30-Base-B":			["X11SPL-F"  ,0,2,"Q30","STORINATOR"],
	"Storinator-H8-Q30-Base":			["X11SSM-F"  ,0,2,"Q30","STORINATOR"],
	"Storinator-H8-Q30-Enhanced":		["X11SPL-F"  ,0,2,"Q30","STORINATOR"],
	"Storinator-H8-Q30-Enhanced-S":		["X11SPL-F"  ,0,2,"Q30","STORINATOR"],
	"Storinator-H8-Q30-Enhanced-AMD":	["H11SSL-i"  ,0,2,"Q30","STORINATOR"],
	"Storinator-H8-Q30-Turbo":			["X11DPL-i"  ,0,2,"Q30","STORINATOR"],
	"Storinator-H8-Q30-Turbo-G":		["X11SPL-F"  ,0,2,"Q30","STORINATOR"],

	"Storinator-H8-S45-Base-B":			["X11SPL-F"  ,0,3,"S45","STORINATOR"],
	"Storinator-H8-S45-Base":			["X11SSM-F"  ,0,3,"S45","STORINATOR"],
	"Storinator-H8-S45-Enhanced":		["X11SPL-F"  ,0,3,"S45","STORINATOR"],
	"Storinator-H8-S45-Enhanced-S":		["X11SPL-F"  ,0,3,"S45","STORINATOR"],
	"Storinator-H8-S45-Enhanced-AMD":	["H11SSL-i"  ,0,3,"S45","STORINATOR"],
	"Storinator-H8-S45-Turbo":			["X11DPL-i"  ,0,3,"S45","STORINATOR"],
	"Storinator-H8-S45-Turbo-G":		["X11SPL-F"  ,0,3,"S45","STORINATOR"],

	"Storinator-H8-XL60-Enhanced":		["X11SPL-F"  ,0,4,"XL60","STORINATOR"],
	"Storinator-H8-XL60-Enhanced-S":	["X11SPL-F"  ,0,4,"XL60","STORINATOR"],
	"Storinator-H8-XL60-Enhanced-AMD":	["H11SSL-i"  ,0,4,"XL60","STORINATOR"],
	"Storinator-H8-XL60-Turbo":			["X11DPL-i"  ,0,4,"XL60","STORINATOR"],
	"Storinator-H8-XL60-Turbo-G":		["X11SPL-F"  ,0,4,"XL60","STORINATOR"],

	"Storinator-H16-AV15-Enhanced":		["X11SPL-F"  ,1,0,"AV15" ,"H16"],
	"Storinator-H16-AV15-Enhanced-S":	["X11SPL-F"  ,1,0,"AV15" ,"H16"],
	"Storinator-H16-AV15-Enhanced-AMD":	["H11SSL-i"  ,1,0,"AV15" ,"H16"],
	"Storinator-H16-AV15-Turbo":		["X11DPL-i"  ,1,0,"AV15" ,"H16"],
	"Storinator-H16-AV15-Turbo-G":		["X11SPL-F"  ,1,0,"AV15" ,"H16"],

	"Storinator-H16-AV15-Enhanced-AMD":	["X11SPL-F"  ,1,0,"AV15" ,"H16"],
	"Storinator-H16-Q30-Enhanced":		["X11SPL-F"  ,1,1,"Q30" ,"H16"],
	"Storinator-H16-Q30-Enhanced-S":	["X11SPL-F"  ,1,1,"Q30" ,"H16"],
	"Storinator-H16-Q30-Enhanced-AMD":	["H11SSL-i"  ,1,1,"Q30" ,"H16"],
	"Storinator-H16-Q30-Turbo":			["X11DPL-i"  ,1,1,"Q30" ,"H16"],
	"Storinator-H16-Q30-Turbo-G":		["X11SPL-F"  ,1,1,"Q30" ,"H16"],
	"Storinator-H16-S45-Enhanced":		["X11SPL-F"  ,1,2,"S45" ,"H16"],
	"Storinator-H16-S45-Enhanced-S":	["X11SPL-F"  ,1,2,"S45" ,"H16"],
	"Storinator-H16-S45-Enhanced-AMD":	["H11SSL-i"  ,1,2,"S45" ,"H16"],
	"Storinator-H16-S45-Turbo":			["X11DPL-i"  ,1,2,"S45" ,"H16"],
	"Storinator-H16-S45-Turbo-G":		["X11SPL-F"  ,1,2,"S45" ,"H16"],
	"Storinator-H16-XL60-Enhanced":		["X11SPL-F"  ,1,3,"XL60","H16"],
	"Storinator-H16-XL60-Enhanced-S":	["X11SPL-F"  ,1,3,"XL60","H16"],
	"Storinator-H16-XL60-Enhanced-AMD":	["H11SSL-i"  ,1,3,"XL60","H16"],
	"Storinator-H16-XL60-Turbo":		["X11DPL-i"  ,1,3,"XL60","H16"],
	"Storinator-H16-XL60-Turbo-G":		["X11SPL-F"  ,1,3,"XL60","H16"],

	"Storinator-H32-Q30-Enhanced":		["X11SPL-F"  ,2,0,"Q30" ,"H32"],
	"Storinator-H32-Q30-Enhanced-S":	["X11SPL-F"  ,2,0,"Q30" ,"H32"],
	"Storinator-H32-Q30-Enhanced-AMD":	["H11SSL-i"  ,2,0,"Q30" ,"H32"],
	"Storinator-H32-Q30-Turbo":			["X11DPL-i"  ,2,0,"Q30" ,"H32"],
	"Storinator-H32-Q30-Turbo-G":		["X11SPL-F"  ,2,0,"Q30" ,"H32"],
	"Storinator-H32-S45-Enhanced":		["X11SPL-F"  ,2,1,"S45" ,"H32"],
	"Storinator-H32-S45-Enhanced-S":	["X11SPL-F"  ,2,1,"S45" ,"H32"],
	"Storinator-H32-S45-Enhanced-AMD":	["H11SSL-i"  ,2,1,"S45" ,"H32"],
	"Storinator-H32-S45-Turbo":			["X11DPL-i"  ,2,1,"S45" ,"H32"],
	"Storinator-H32-S45-Turbo-G":		["X11SPL-F"  ,2,1,"S45" ,"H32"],
	"Storinator-H32-XL60-Enhanced":		["X11SPL-F"  ,2,2,"XL60","H32"],
	"Storinator-H32-XL60-Enhanced-S":	["X11SPL-F"  ,2,2,"XL60","H32"],
	"Storinator-H32-XL60-Enhanced-AMD":	["H11SSL-i"  ,2,2,"XL60","H32"],
	"Storinator-H32-XL60-Turbo":		["X11DPL-i"  ,2,2,"XL60","H32"],
	"Storinator-H32-XL60-Turbo-G":		["X11SPL-F"  ,2,2,"XL60","H32"],

	"Storinator-MI4-Base":				["X11SSM-F"  ,0,0,"MI4"  ,"STORINATOR"],
	"Storinator-MI4-Base-B":			["X11SPL-F"  ,0,0,"MI4"  ,"STORINATOR"],
	"Storinator-MI4-Enhanced":			["X11SPL-F"  ,0,0,"MI4"  ,"STORINATOR"],
	"Storinator-MI4-Enhanced-S":		["X11SPL-F"  ,0,0,"MI4"  ,"STORINATOR"],
	"Storinator-MI4-Enhanced-AMD":		["H11SSL-i"  ,0,0,"MI4"  ,"STORINATOR"],
	"Storinator-MI4-Turbo":				["X11DPL-i"  ,0,0,"MI4"  ,"STORINATOR"],
	"Storinator-MI4-Turbo-G":			["X11SPL-F"  ,0,0,"MI4"  ,"STORINATOR"],

	"Storinator-Q30-Base":				["X11SSM-F"  ,0,2,"Q30" ,"STORINATOR"],
	"Storinator-Q30-Base-B":			["X11SPL-F"  ,0,2,"Q30" ,"STORINATOR"],
	"Storinator-Q30-Enhanced":			["X11SPL-F"  ,0,2,"Q30" ,"STORINATOR"],
	"Storinator-Q30-Enhanced-S":		["X11SPL-F"  ,0,2,"Q30" ,"STORINATOR"],
	"Storinator-Q30-Enhanced-AMD":		["H11SSL-i"  ,0,2,"Q30" ,"STORINATOR"],
	"Storinator-Q30-Turbo":				["X11DPL-i"  ,0,2,"Q30" ,"STORINATOR"],
	"Storinator-Q30-Turbo-G":			["X11SPL-F"  ,0,2,"Q30" ,"STORINATOR"],

	"Storinator-S45-Base":				["X11SSM-F"  ,0,3,"S45" ,"STORINATOR"],
	"Storinator-S45-Base-B":			["X11SPL-F"  ,0,3,"S45" ,"STORINATOR"],
	"Storinator-S45-Enhanced":			["X11SPL-F"  ,0,3,"S45" ,"STORINATOR"],
	"Storinator-S45-Enhanced-S":		["X11SPL-F"  ,0,3,"S45" ,"STORINATOR"],
	"Storinator-S45-Enhanced-AMD":		["H11SSL-i"  ,0,3,"S45" ,"STORINATOR"],
	"Storinator-S45-Turbo":				["X11DPL-i"  ,0,3,"S45" ,"STORINATOR"],
	"Storinator-S45-Turbo-G":			["X11SPL-F"  ,0,3,"S45" ,"STORINATOR"],

	"Storinator-XL60-Enhanced":			["X11SPL-F"  ,0,4,"XL60","STORINATOR"],
	"Storinator-XL60-Enhanced-S":		["X11SPL-F"  ,0,4,"XL60","STORINATOR"],
	"Storinator-XL60-Enhanced-AMD":		["H11SSL-i"  ,0,4,"XL60","STORINATOR"],
	"Storinator-XL60-Turbo":			["X11DPL-i"  ,0,4,"XL60","STORINATOR"],
	"Storinator-XL60-Turbo-G":			["X11SPL-F"  ,0,4,"XL60","STORINATOR"],

	"Stornado-AV15-Enhanced":			["X11SPL-F"  ,0,2,"AV15","STORNADO"],
	"Stornado-AV15-Enhanced-S":			["X11SPL-F"  ,0,2,"AV15","STORNADO"],
	"Stornado-AV15-Enhanced-AMD":		["H11SSL-i"  ,0,2,"AV15","STORNADO"],
	"Stornado-AV15-Turbo":				["X11DPL-i"  ,0,2,"AV15","STORNADO"],
	"Stornado-AV15-Turbo-G":			["X11SPL-F"  ,0,2,"AV15","STORNADO"],

	"Stornado-2U-Base-B":				["X11SPL-F"  ,0,2,"2U","2USTORNADO"],
	"Stornado-2U-Enhanced-AMD":			["X11SPL-F"  ,0,2,"2U","2USTORNADO"],
	"Stornado-2U-Enhanced-S":			["X11SPL-F"  ,0,2,"2U","2USTORNADO"],
	"Stornado-2U-Turbo-G":				["X11SPL-F"  ,0,2,"2U","2USTORNADO"],
	"Stornado-2U-Turbo":				["X11DPL-i"  ,0,2,"2U","2USTORNADO"],

	"Destroyinator-AV15-Enhanced":		["X11SPL-F",0,1,"AV15","DESTROYINATOR"],
	"Destroyinator-AV15-Enhanced-S":	["X11SPL-F",0,1,"AV15","DESTROYINATOR"],
	"Destroyinator-Q30-Enhanced":		["X11SPL-F",0,2,"Q30","DESTROYINATOR"],
	"Destroyinator-Q30-Enhanced-S":		["X11SPL-F",0,2,"Q30","DESTROYINATOR"],
	"Destroyinator-S45-Enhanced":		["X11SPL-F",0,3,"S45","DESTROYINATOR"],
	"Destroyinator-S45-Enhanced-S":		["X11SPL-F",0,3,"S45","DESTROYINATOR"],
	"Destroyinator-XL60-Enhanced":		["X11SPL-F",0,4,"XL60","DESTROYINATOR"],
	"Destroyinator-XL60-Enhanced-S":	["X11SPL-F",0,4,"XL60","DESTROYINATOR"],

	"Storinator-AV15-VM":				["VIRTUAL_MACHINE",0,1,"AV15","STORINATOR"],
	"Storinator-Q30-VM":				["VIRTUAL_MACHINE",0,1,"AV15","STORINATOR"],
	"Storinator-S45-VM":				["VIRTUAL_MACHINE",0,1,"AV15","STORINATOR"],
	"Storinator-XL60-VM":				["VIRTUAL_MACHINE",0,1,"AV15","STORINATOR"],

	"Storinator":						["?",0,0,"?","STORINATOR"],
	"?":								["?",0,0,"?","?"]
}

def motherboard():
	# search through the output of the command "dmidecode -t 2" for motherboard information
	# example output:
		# # dmidecode 3.2
		# Getting SMBIOS data from sysfs.
		# SMBIOS 3.2.1 present.
		# # SMBIOS implementations newer than version 3.2.0 are not
		# # fully supported by this version of dmidecode.
		
		# Handle 0x0002, DMI type 2, 15 bytes
		# Base Board Information
		#         Manufacturer: Supermicro
		#         Product Name: X11SPL-F
		#         Version: 1.01
		#         Serial Number: ZM18AS011320
		#         Asset Tag: To be filled by O.E.M.
		#         Features:
		#                 Board is a hosting board
		#                 Board is replaceable
		#         Location In Chassis: To be filled by O.E.M.
		#         Chassis Handle: 0x0003
		#         Type: Motherboard
		#         Contained Object Handles: 0

	mobo_dict = {
	"Manufacturer":"?",
	"Product Name":"?",
	"Serial Number":"?"
	}

	mobo = []

	try:
		dmi_result = subprocess.Popen(["dmidecode","-t","2"],stdout=subprocess.PIPE,universal_newlines=True).stdout
	except:
		print("ERROR: dmidecode is not installed")
		exit(1)
	for line in dmi_result:
		for field in mobo_dict.keys():
			regex = re.search("^\s({fld}):\s+(.*)".format(fld=field),line)
			if regex != None:
					mobo_dict[regex.group(1)] = regex.group(2)
	
	return mobo_dict

def getDmidecodePCI():
    try:
        dmidecode_result = subprocess.Popen(
            ["dmidecode", "-t", "9"], stdout=subprocess.PIPE, universal_newlines=True).stdout.read()
    except:
        print("ERROR: dmidecode not installed")
        exit(1)
    pci_slots = []
    rx_pci=re.compile(r"^Handle.*\n(?:.|\n)+?^.*(?:Designation:\s+).*SLOT(.).*(?:.|\n)+?^.*Bus Address:\s+(\S+).*",re.MULTILINE)
    for match in rx_pci.finditer(dmidecode_result):
        pci_slots.append({"ID":match.group(1),"Bus Address":match.group(2)})

    return pci_slots


def hba_lspci():
	# determine the model and count of hba cards present in the system
	# by parsing the output of "lspci -d 1000:* -vv -i /opt/45drives/tools/pci.ids"
	# example output:
	#17:00.0 RAID bus controller: Broadcom / LSI Fusion-MPT 24GSAS/PCIe SAS40xx (rev 01)
	#    Subsystem: Broadcom / LSI eHBA 9600-24i Tri-Mode Storage Adapter
	#    Kernel driver in use: mpi3mr
	#    Kernel modules: mpi3mr
	#65:00.0 RAID bus controller: Broadcom / LSI Fusion-MPT 24GSAS/PCIe SAS40xx (rev 01)
	#    Subsystem: Broadcom / LSI eHBA 9600-16i Tri-Mode Storage Adapter
	#    Kernel driver in use: mpi3mr
	#    Kernel modules: mpi3mr

	hba_count = 0
	hba = []
	hba_models = {
		"SAS9305-16i":16,
		"SAS9305-24i":24,
		"HBA 9405W-16i":16,
		"9600-24i":24,
		"9600-16i":16
	}

	lspci_output = []

	try:
		lspci_result = subprocess.Popen(["lspci", "-d", "1000:*","-vv", "-i", "/opt/45drives/tools/pci.ids"],stdout=subprocess.PIPE,universal_newlines=True).stdout.read()
	except:
		print("ERROR: error running lspci -d 1000:* -vv")
		exit(1)

	hba_drivers = {
		"SAS9305-16i":"mpt3sas",
		"SAS9305-24i":"mpt3sas",
		"HBA 9405W-16i":"mpt3sas",
		"9600-24i":"mpi3mr",
		"9600-16i":"mpi3mr"
	}

	hba_adapters = {
		"SAS9305-16i":"SAS3224 PCI-Express Fusion-MPT SAS-3",
		"SAS9305-24i":"SAS3224 PCI-Express Fusion-MPT SAS-3",
		"HBA 9405W-16i":"SAS3616 Fusion-MPT Tri-Mode I/O Controller Chip (IOC)",
		"9600-24i":"Fusion-MPT 24GSAS/PCIe SAS40xx",
		"9600-16i":"Fusion-MPT 24GSAS/PCIe SAS40xx"
	}

	hybrid_flag = False

	hba_dict = {
		"Model":"?",
		"Adapter":"?",
		"Bus Address":"?",
		"Drive Connections":0,
		"Kernel Driver":"?",
		"Ctl":0
	}

	rx_pci=re.compile(r"^(\w\w:\w\w\.\w).*\n.*(?:(?:(?:^\t).*\n)+^.*)?(9600-16i|9600-24i|SAS9305-16i|SAS9305-24i|HBA 9405W-16i).*\n",re.MULTILINE)
	ctl = 0
	for match in rx_pci.finditer(lspci_result):
		hba_dict["Model"] = match.group(2)
		hba_dict["Adapter"] = hba_adapters[match.group(2)]
		hba_dict["Bus Address"] = match.group(1)
		hba_dict["Drive Connections"] = hba_models[match.group(2)]
		hba_dict["Kernel Driver"] = hba_drivers[match.group(2)]
		hba_dict["Ctl"] = ctl
		ctl = ctl + 1
		if hba_dict["Drive Connections"] == 24:
			hybrid_flag = True
		hba.append(hba_dict.copy())
	
	if len(hba) != 0:
		# get list of pci devices including their bus address and slot id
		pci_slots = getDmidecodePCI()

		# get a list of pci devices used by system
		sys_bus_path = "/sys/bus/pci/devices"
		try:
			sys_bus_addrs = os.listdir(sys_bus_path)
		except:
			sys_bus_addrs = []

		for pci_slot in pci_slots:
			for card in hba:
				if card["Bus Address"] in pci_slot["Bus Address"]:
					card["PCI Slot"] = pci_slot["ID"]
					if len(sys_bus_addrs) > 0 and pci_slot["Bus Address"] not in sys_bus_addrs:
						# dmidecode gave a bus address that does not match the one used by the system
						for j in range(0,len(sys_bus_addrs)):
							if card["Bus Address"] in sys_bus_addrs[j]:
								# we have found the system bus address that matches the substring
								# address provided by lspci update the card's bus address field
								card["Bus Address"] = sys_bus_addrs[j]
								print("using /sys/bus/pci/devices",sys_bus_addrs[j])
								break
					else:
						# use the bus address provided by dmidecode
						# update the cards bus address to the full format (eg: 0000:01:00.0)
						card["Bus Address"] = pci_slot["Bus Address"]
					
		#sort the final list of hba cards by the Bus Address field. This ensures that
		#the order in which they are stored resembles the lspci output. which has
		#worked reliably in the past.
		hba = sorted(hba, key=lambda k: k['Bus Address']) 

	return hba, hybrid_flag

def hba():
	# determine the model and count of hba cards present in the system
	# by parsing the output of /opt/45drives/tools/storcli64 show.
	# example output:
		# CLI Version = 007.1017.0000.0000 May 10, 2019
		# Operating system = Linux 4.18.0-240.1.1.el8_3.x86_64
		# Status Code = 0
		# Status = Success
		# Description = None
		# 
		# Number of Controllers = 2
		# Host Name = hoop.45lab.com
		# Operating System  = Linux 4.18.0-240.1.1.el8_3.x86_64
		# StoreLib IT Version = 07.1100.0200.0100
		# StoreLib IR3 Version = 16.06-0
		# 
		# IT System Overview :
		# ==================
		# 
		# ----------------------------------------------------------------------------
		# Ctl Model         AdapterType   VendId DevId SubVendId SubDevId PCI Address 
		# ----------------------------------------------------------------------------
		#   0 HBA 9405W-16i   SAS3616(B0) 0x1000  0xD1    0x1000   0x3080 00:17:00:00 
		#   1 HBA 9405W-16i   SAS3616(B0) 0x1000  0xD1    0x1000   0x3080 00:65:00:00 
		# ----------------------------------------------------------------------------
	hba_count = 0
	hba = []
	try:
		storcli_result = subprocess.Popen(["/opt/45drives/tools/storcli64","show"],stdout=subprocess.PIPE,universal_newlines=True).stdout
	except:
		print("ERROR: storcli is not installed")
		exit(1)
	for line in storcli_result:
		regex = re.search("Number of Controllers = (\d)",line)
		if regex != None:
			hba_count = int(regex.group(1))
			break

	hba_models = {
		"SAS9305-16i":16,
		"SAS9305-24i":24,
		"HBA 9405W-16i":16
	}

	hba_drivers = {
		"SAS9305-16i":"mpt3sas",
		"SAS9305-24i":"mpt3sas",
		"HBA 9405W-16i":"mpt3sas"
	}

	hba_dict = {
		"Model":"?",
		"Adapter":"?",
		"Bus Address":"?",
		"Drive Connections":0,
		"Kernel Driver":"?"
	}

	hybrid_flag = False

	if hba_count != 0:
		for line in storcli_result:
			for model in hba_models.keys():
				# Model AdapterType VendId DevId SubVendId SubDevId PCIAddress 	
				regex = re.search("(\d)\s+({fld})\s+(\w+).*\w\w:(\w\w:\w\w:\w\w)\s+$".format(fld=model),line)
				if regex != None:
					hba_dict["Model"] = regex.group(2)
					hba_dict["Adapter"] = regex.group(3)
					hba_dict["Bus Address"] = regex.group(4)
					hba_dict["Bus Address"] = hba_dict["Bus Address"][:-3] + ".0"
					hba_dict["Drive Connections"] = hba_models[model]
					hba_dict["Kernel Driver"] = hba_drivers[model]
					hba_dict["Ctl"] = int(regex.group(1))
					if hba_dict["Drive Connections"] == 24:
						hybrid_flag = True
					hba.append(hba_dict.copy())
		
		# get list of pci devices including their bus address and slot id
		pci_slots = getDmidecodePCI()

		# get a list of pci devices used by system
		sys_bus_path = "/sys/bus/pci/devices"
		try:
			sys_bus_addrs = os.listdir(sys_bus_path)
		except:
			sys_bus_addrs = []

		for pci_slot in pci_slots:
			for card in hba:
				if card["Bus Address"] in pci_slot["Bus Address"]:
					card["PCI Slot"] = pci_slot["ID"]
					if len(sys_bus_addrs) > 0 and pci_slot["Bus Address"] not in sys_bus_addrs:
						# dmidecode gave a bus address that does not match the one used by the system
						for j in range(0,len(sys_bus_addrs)):
							if card["Bus Address"] in sys_bus_addrs[j]:
								# we have found the system bus address that matches the substring
								# address provided by storcli64. update the card's bus address field
								card["Bus Address"] = sys_bus_addrs[j]
								print("using /sys/bus/pci/devices",sys_bus_addrs[j])
								break
					else:
						# use the bus address provided by dmidecode
						# update the cards bus address to the full format (eg: 0000:01:00.0)
						card["Bus Address"] = pci_slot["Bus Address"]
					
		#sort the final list of hba cards by the Bus Address field. This ensures that
		#the order in which they are stored resembles the lspci output. which has
		#worked reliably in the past.
		hba = sorted(hba, key=lambda k: k['Bus Address']) 

	return hba, hybrid_flag

def serial_check():
	#### OLD SERIAL ######################################
	# FRU Device Description : Builtin FRU Device (ID 0)
	# Chassis Type          : Unspecified
	# Chassis Part Number   : N/A
	# Chassis Serial        : N/A
	# Board Mfg Date        : Tue Dec  1 05:04:00 2020
	# Board Mfg             : Supermicro
	# Board Product         : X11SPL-F
	# Board Serial          : 1238383213
	# Board Part Number     : N/A
	# Product Manufacturer  : 45Drives
	# Product Name          : Storinator
	# Product Part Number   : S45
	# Product Version       : 5.0
	# Product Serial        : 1234-1
	# Product Asset Tag     : N/A

	#### NEW SERIAL #####################################
	# FRU Device Description : Builtin FRU Device (ID 0)
	# Chassis Type          : Rack Mount Chassis
	# Chassis Part Number   : AV15
	# Chassis Serial        : 13371337-1
	# Board Mfg Date        : Tue Dec  1 05:04:00 2020
	# Board Mfg             : Supermicro
	# Board Product         : X11SPL-F
	# Board Serial          : ZM18AS011320
	# Board Part Number     : X11SPL-F
	# Product Manufacturer  : 45Drives
	# Product Name          : Stornado-AV15-Enhanced
	# Product Part Number   : AV15
	# Product Version       : (Enhanced)
	# Product Serial        : 13371337-1
	# Product Asset Tag     : IPMIPASSWD
	serial_fields = [
		"Chassis Type",
		"Chassis Part Number",
		"Chassis Serial",
		"Board Mfg Date",
		"Board Mfg",
		"Board Product",
		"Board Serial",
		"Board Part Number",
		"Product Manufacturer",
		"Product Name",
		"Product Part Number",
		"Product Version",
		"Product Serial",
		"Product Asset Tag"
	]

	serial_result = {
		"Chassis Type":"?",
		"Chassis Part Number":"?",
		"Chassis Serial":"?",
		"Board Mfg Date":"?",
		"Board Mfg":"?",
		"Board Product":"?",
		"Board Serial":"?",
		"Board Part Number":"?",
		"Product Manufacturer":"?",
		"Product Name":"?",
		"Product Part Number":"?",
		"Product Version":"?",
		"Product Serial":"?",
		"Product Asset Tag":"?"
	}

	try:
		ipmi_test = subprocess.run(["ipmitool","fru"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL).returncode
	except:
		print("ERROR: ipmitool is not installed")
		exit(1)
	
	if ipmi_test:
		# ipmitool fru command has returned a standard error. This can happen on a virtual machine
		print("/opt/45drives/tools/server_identifier: ipmitool fru command failed.")
		print("/opt/45drives/tools/server_identifier: !! WARNING !! Cannot determine server type automatically using non-standard hardware.")
		return serial_result

	
	ipmi_result = subprocess.Popen(["ipmitool","fru"],stdout=subprocess.PIPE,universal_newlines=True).stdout
	for line in ipmi_result:
		for field in serial_fields:
			regex = re.search("({fld})\s+:\s+(\S+)".format(fld=field),line)
			if regex != None:
				serial_result[regex.group(1)] = regex.group(2)
	return serial_result

def determine_model(mobo_model_str,hba_dict_lst,chassis_size_str):
	hba_16i_count = 0
	hba_24i_count = 0

	model = "?"
	for card in hba_dict_lst:
		if card["Drive Connections"] == 16:
			hba_16i_count += 1
		elif card["Drive Connections"] == 24:
			hba_24i_count += 1

	for sys_type in g_product_lut.keys():
		if (g_product_lut[sys_type][g_product_lut_idx["MOBO_MODEL"]] == mobo_model_str and
			g_product_lut[sys_type][g_product_lut_idx["24I_COUNT"]] == hba_24i_count and
			g_product_lut[sys_type][g_product_lut_idx["16I_COUNT"]] == hba_16i_count and
			g_product_lut[sys_type][g_product_lut_idx["CHASSIS_SIZE"]] == chassis_size_str):
				model = sys_type

	if model == "?":
		if mobo_model_str != "?":
			print("/opt/45drives/tools/server_identifier: !! WARNING !! " + mobo_model_str + " Motherboard is not supported for Automatic Identification.")
		print("/opt/45drives/tools/server_identifier: !! WARNING !! Automatic Identification failed. ")
		print("/opt/45drives/tools/server_identifier: Setting Model to \"Storinator\"(Generic)")
		model = "Storinator"

	return model

def update_json_file(server,scan_time):
	old_file = None
	old_server = None
	json_dir = "/etc/45drives/server_info"

	#make a directory to store the server info files
	if not os.path.isdir(json_dir):
		print("/opt/45drives/tools/server_identifier: Server Info Directory Created:  ("+json_dir+")")
		os.makedirs(json_dir)

	# load the existing server_info.json file in as a json object.
	if os.path.exists(json_dir+"/server_info.json") and os.path.isfile(json_dir+"/server_info.json"):
		old_file = open(json_dir+"/server_info.json","r")
		try:
			old_server = json.load(old_file)
		except Exception as e:
			print("/opt/45drives/tools/server_identifier: Overwriting "+json_dir+"server_info.json with scan results.")
			old_server = None
		old_file.close()

	if old_server != None:
		# json object loaded in successfully
		if "Edit Mode" not in old_server.keys():
			old_server["Edit Mode"] = False

		if old_server != server and server["Model"] != "?" and not old_server["Edit Mode"]:
			# the hardware configuration has changed since last time
			# back up the existing server_info_file
			backup_file = open(json_dir+"/server_info_backup_"+scan_time+".json","w")
			backup_file.write(json.dumps(old_server,indent=4))
			backup_file.close()

			print("/opt/45drives/tools/server_identifier: Hardware configuration has changed since last scan.")
			print("                              A backup has been created in ("+json_dir+"/server_info_backup_"+scan_time+".json)")
			
			new_file = open(json_dir+"/server_info.json","w")
			new_file.write(json.dumps(server,indent=4))
			new_file.write("\n")
			new_file.close()
			print("--------------------------------------------------------------------------------")
			print(json_dir+"/server_info_backup_"+scan_time+".json)")
			print("--------------------------------------------------------------------------------")
			print(json.dumps(old_server,indent=4))
			print("--------------------------------------------------------------------------------")
			print(json_dir+"/server_info.json:")
			print("--------------------------------------------------------------------------------")
			print(json.dumps(server,indent=4))

		elif old_server != server and old_server["Edit Mode"]:
			print("/opt/45drives/tools/server_identifier: Hardware configuration has changed since last scan.")
			print("                              \"Edit Mode\" is enabled. ("+json_dir+"/server_info.json)")
			print("                              Results of this scan will not be saved.")

		if old_server["Edit Mode"]:
			print("--------------------------------------------------------------------------------")
			print(json_dir+"/server_info.json:")
			print("--------------------------------------------------------------------------------")
			print(json.dumps(old_server,indent=4))
			print("--------------------------------------------------------------------------------")
			print("Scan Results: (\"Edit Mode\":true i.e. server_info.json will not be modified)")
			print("--------------------------------------------------------------------------------")
			print(json.dumps(server,indent=4))
		else:
			print("--------------------------------------------------------------------------------")
			print("Scan Results: ")
			print("--------------------------------------------------------------------------------")
			print(json.dumps(server,indent=4))


	elif server["Model"] != "?":
		# this is the first time that this script was run on the system successfully
		new_file = open(json_dir+"/server_info.json","w")
		new_file.write(json.dumps(server,indent=4))
		new_file.write("\n")
		new_file.close()
		print("/opt/45drives/tools/server_identifier: Server Info File Created:  ("+json_dir+"/server_info.json)")
		print("--------------------------------------------------------------------------------")
		print(json_dir+"/server_info.json:")
		print("--------------------------------------------------------------------------------")
		print(json.dumps(server,indent=4))
	
	else:
		# this was unsuccessful. Write the info file for the first time to store the result.
		print("/opt/45drives/tools/server_identifier: Placeholder Server Info File Created:  ("+json_dir+"/server_info.json)")
		new_file = open(json_dir+"/server_info.json","w")
		new_file.write(json.dumps(server,indent=4))
		new_file.write("\n")
		new_file.close()
		print("--------------------------------------------------------------------------------")
		print(json_dir+"/server_info.json:")
		print("--------------------------------------------------------------------------------")
		print(json.dumps(server,indent=4))

	if server["Model"] == "?":
		print("/opt/45drives/tools/server_identifier: !! WARNING !!")
		print("                              Unable to determine server model automatically.")
		print("                              Server Info File Path:  ("+json_dir+"/server_info.json)")
		print("                              You can edit the server_info file manually and run dmap again if using non-standard hardware.")
		print("                              If using standard 45Drives server hardware. Serialization should be performed before running dmap.")

def vm_check(mobo_dict):
	return (mobo_dict["Manufacturer"] == "?" and mobo_dict["Product Name"] == "?" and mobo_dict["Serial Number"] == "?")

def old_serial(serial_result):
	for field in serial_result.keys():
		if serial_result[field] == "N/A":
			return True
	return False

def vm_passthrough(server):
	server["Model"] = "Storinator-{cs}-VM".format(cs=g_chassis_sizes[len(server["HBA"])])
	server["Chassis Size"] = g_chassis_sizes[len(server["HBA"])]
	server["Motherboard"]["Serial Number"] = "VIRTUAL_MACHINE"
	server["Motherboard"]["Manufacturer"] = "VIRTUAL_MACHINE"
	server["Motherboard"]["Product Name"] = "VM_MOTHERBOARD"
	server["Serial"] = "VIRTUAL_MACHINE"
	print("/opt/45drives/tools/server_identifier: Virtual Machine with HBA Pass Through Detected.")
	print("                              Setting Model to \"{m}\", and Chassis Size to \"{c}\"".format(m=server["Model"],c=server["Chassis Size"]))
	try:
		lspci_result = subprocess.Popen(["lspci"],stdout=subprocess.PIPE,universal_newlines=True, stderr=subprocess.PIPE).stdout
	except:
		print("ERROR: lspci command failed")
		exit(1)
	for hba_card in server["HBA"]:
		for line in lspci_result:
			regex = re.search("^({addr}).*{adap}".format(addr=hba_card["Bus Address"],adap=hba_card["Adapter"]),line)
			if regex != None:
				hba_card["Bus Address"] = "0000:" + regex.group(1)

def edit_mode_check():
	old_file = None
	old_server = None
	json_dir = "/etc/45drives/server_info"

	#make a directory to store the server info files
	if not os.path.isdir(json_dir):
		print("/opt/45drives/tools/server_identifier: Server Info Directory Created:  ("+json_dir+")")
		os.makedirs(json_dir)

	# load the existing server_info.json file in as a json object.
	if os.path.exists(json_dir+"/server_info.json") and os.path.isfile(json_dir+"/server_info.json"):
		old_file = open(json_dir+"/server_info.json","r")
		try:
			old_server = json.load(old_file)
		except Exception as e:
			print("/opt/45drives/tools/server_identifier: Error loading data from " + json_dir + "/server_info.json")
			print("Error Message: ",e)
			if input("/opt/45drives/tools/server_identifier: Would you like to overwrite existing file with new scan results? (y/n):") == "n":
				print("/opt/45drives/tools/server_identifier: Make the necessary adjustments to "+ json_dir + "/server_info.json and try again.")
				old_file.close()
				sys.exit(1)
			old_server = None
		old_file.close()

	if old_server != None:
		# json object loaded in successfully
		if "Edit Mode" not in old_server.keys():
			old_server["Edit Mode"] = False
		return old_server["Edit Mode"]
	return False

def get_os():
	os_release_path = "/etc/os-release"
	os_release_fields = {
		"NAME":"?",
		"VERSION_ID":"?"
		}
	if os.path.isfile(os_release_path):
		os_release_file = open(os_release_path,"r")
		os_release_lines = os_release_file.read().splitlines()
		os_release_file.close()
		for line in os_release_lines:
			for field in os_release_fields.keys():
				regex = re.search("^({fld})=".format(fld=field) + '\"(.+?)\"',line)
				if regex != None:
					os_release_fields[regex.group(1)] = regex.group(2)

	return os_release_fields["NAME"],os_release_fields["VERSION_ID"]


def main():
	server = {
		"Motherboard":"?",
		"HBA":[],
		"Hybrid":False,
		"Serial":"?",
		"Model":"?",
		"Alias Style":"?",
		"Chassis Size":"?",
		"VM":False,
		"Edit Mode":False,
		"OS NAME": "?",
		"OS VERSION_ID": "?"
	}
	# get current time
	current_day = datetime.today()
	current_time = datetime.now()
	scan_time = current_day.strftime("%Y_%m_%d_") + current_time.strftime("%H_%M")
	server["Edit Mode"] = edit_mode_check()
	serial_result = {}
	
	server["Motherboard"] = motherboard()
	#server["HBA"], server["Hybrid"] = hba() <- depreciated..
	server["HBA"], server["Hybrid"] = hba_lspci()
	server["VM"] = vm_check(server["Motherboard"])
	if not server["VM"]:
		serial_result = serial_check()
		server["Serial"] = serial_result["Product Serial"].upper()
		server["Chassis Size"] = serial_result["Product Part Number"].upper()
		server["Model"] = determine_model(server["Motherboard"]["Product Name"],server["HBA"],server["Chassis Size"]) if old_serial(serial_result) else serial_result["Product Name"]

	if server["Model"] == "?":
		if len(server["HBA"]) > 0 and server["VM"]:
			vm_passthrough(server) 			# VM with hba pass through detected, update server accordingly

	server["Alias Style"] = g_product_lut[server["Model"]][g_product_lut_idx["ALIAS_STYLE"]]
	
	# This is added to use only a single chassis size for the Stornado
	if "Product Part Number" in serial_result.keys():
		if serial_result["Product Part Number"].upper() == "F32":
			server["Alias Style"] = "STORNADO"
			server["Chassis Size"] = "AV15"

	# get OS NAME and OS VERSION_ID
	server["OS NAME"], server["OS VERSION_ID"] = get_os()
	update_json_file(server,scan_time)

	# warn user if improper number of HBA cards are detected.
	if len(server["HBA"]) != g_product_lut[server["Model"]][g_product_lut_idx["24I_COUNT"]] + g_product_lut[server["Model"]][g_product_lut_idx["16I_COUNT"]]:
		print("/opt/45drives/tools/server_identifier: !! WARNING !!")
		print("                              Quantity of HBA Cards detected does not match the quantity expected")
		print("                              for a \"{m}\". Drive aliasing may not work as desired. ".format(m=server["Model"]))
	exit(0)

if __name__ == "__main__":
    main()
