#!/usr/libexec/platform-python
################################################################################
# network:
# 	used to return information about the network configuration in a .json
#   format. This is a helper sctipt for use with the
#   cockpit-hardware package (https://github.com/45Drives/cockpit-hardware)
#
# Copyright (C) 2020, 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 json
from optparse import OptionParser

g_dmi_fields = [
	"Bus Address",
	"Designation",
	"Type"
]

def get_ipv4(addr_info):
	if not addr_info:
		return "-"

	for addr in addr_info:
		if addr["family"] == "inet":
			return "{local}/{pre}".format(local=addr["local"],pre=str(addr["prefixlen"]))
	
	return "-"

def get_ipv6(addr_info):
	if not addr_info:
		return "-"

	for addr in addr_info:
		if addr["family"] == "inet6":
			return "{local}/{pre}".format(local=addr["local"],pre=str(addr["prefixlen"]))

	return "-"

def ip_addr():
	try:
		ipaddr_result = subprocess.Popen(
			["ip","-j","addr"],stdout=subprocess.PIPE, universal_newlines=True).stdout
	except:
		errorMessage = {"msg": "error running 'ip -j addr' command. "}
		print(json.dumps(errorMessage))
		exit(1)
	#ipaddr_json = json.loads(ipaddr_result.read())
	try:
		ipaddr_json = json.loads(ipaddr_result.read())
	except:
		errorMessage = {"msg": "error parsing json output of 'ip -j addr' command."}
		print(json.dumps(errorMessage))
		exit(1)
	
	j_ipaddr = []
	for connection in ipaddr_json:
		j_ipaddr.append({
			"Connection Name": connection["ifname"],
			"Connection State": connection["operstate"],
			"Type": connection["link_type"],
			"MAC": connection["address"] if "address" in connection.keys() else "-",
			"IPv4": get_ipv4(connection["addr_info"].copy()),
			"IPv6": get_ipv6(connection["addr_info"].copy())
		})
	return j_ipaddr


def get_network_info():
	#run ip -j addr
	j_connections = ip_addr()

	# LSHW COMMAND:
	# This runs the command: lshw -C network -businfo -quiet
	# this will allow us to determine which pci bus address is associated with a 
	# given connection. The results are stored as a list of tuples 
	# with the format of [(BUS ADDRESS 1, Connection Name 1),(BUS ADDRESS 2, Connection Name 2), ...]
	# the "network" option ensures that only pci devices associated with a given 
	# network connection are displayed. This is important in determining what pci network 
	# cards are installed in the system.
	try:
		lshw_result = subprocess.Popen(
			["lshw","-C","network","-businfo","-quiet"],stdout=subprocess.PIPE, universal_newlines=True).stdout
	except:
		return False
	lshw = []
	for line in lshw_result:
		regex_lshw = re.search("^pci@(\S+)\s+(\S+)",line)
		if regex_lshw != None:
			lshw.append([regex_lshw.group(1),regex_lshw.group(2)])

	# DMIDECODE COMMAND:
	# This runs the command: dmidecode -t 9
	# This lists all pci slot information. The results are stored in 
	# a list called "slot_entries". The g_dmi_fields list stores
	# the fields that we want to parse from the dmidecode output.
	# This list is then seperated into a list of dicts called "cards".
	# of these cards, we then search for cards with bus addresses
	# that match the bus addresses from the lshw command run prior. 
	# Because of the nature of the dmidecode results, we will only
	# see bus addresses in the form of 00:XX:00.0 the .0 results 
	# are the only ones that show up, the bus addresses ending in .1
	# do not. Therefore we have to add these to a list of dicts 
	# manually. This list is called "final_card_list". 
	# Lastly, we load in the netowrk_json_str and append the final_card_list
	# fields to the relevant connection, using the Connection Name 
	# as the method of determining the correct match. 
	try:
		dmi_result = subprocess.Popen(
			["dmidecode","-t","9"],stdout=subprocess.PIPE, universal_newlines=True).stdout
	except:
		return False

	# get the relevant fields from the dmidecode output and store them in a list. 
	slot_entries = []
	for line in dmi_result:
		for field in g_dmi_fields:
			regex = re.search("^\s+({fld}):\s+(.*)".format(fld=field),line)
			if regex != None:
				slot_entries.append((regex.group(1),regex.group(2)))

	# Take the subset of fields in the list for each card and 
	# append them as a dictionary into a new list. 
	cards = []
	for i in range(0,len(slot_entries),len(g_dmi_fields)):
		cards.append(dict(slot_entries[i:i+len(g_dmi_fields)]))

	BA_EPC612D8A = {
		"ff00:16:00.0":"0000:17:00.0", #PCIE1
		"ff00:16:02.0":"0000:1c:00.0", #PCIE2
		"ff00:64:00.0":"0000:65:00.0", #PCIE4
		"ff00:64:02.0":"0000:66:00.0", #PCIE3
		"ff00:b2:00.0":"0000:b3:00.0", #PCIE6
		"ff00:b2:02.0":"0000:b4:00.0" #PCIE5
	}

	for card in cards:
		if "Bus Address" in card.keys() and card["Bus Address"] in BA_EPC612D8A.keys():
			card["Bus Address"] = BA_EPC612D8A[card["Bus Address"]]

	# for each card in the list of dictionaries, append 
	# dictionaries to a list that have the same Bus address
	# as those found in the list of connections found in 
	# the lshw command
	valid_cards = []
	for card in cards:
		for hw_entry in lshw:
			if card["Bus Address"] == hw_entry[0]:
				card["Connection Name"] = hw_entry[1]
				valid_cards.append(card)
				break

	# Copy the dictionaries for each card and modify the last digit of
	# both the Connection Name (ex enp24s0f0) and the bus address (ex. 0000:18:00.0)
	# to "enp24s0f1" and "0000:18:00.1" respectively. This is because we will
	# ALWAYS use pci cards with two ports, and never a single port according to Brett.  
	final_card_lst = []
	for card in valid_cards:
		card_duplicate = card.copy()
		card_duplicate["Bus Address"] = card_duplicate["Bus Address"][:-1] + "1"
		card_duplicate["Connection Name"] = card_duplicate["Connection Name"][:-1] + "1"
		final_card_lst.append(card)
		final_card_lst.append(card_duplicate)


	# add the relevant fields to the list of connections when we have a matching
	# Connection Name
	for obj in j_connections:
		for card in final_card_lst:		
			if obj["Connection Name"] == card["Connection Name"]:
				obj["Bus Address"] = card["Bus Address"]
				obj["Designation"] = card["Designation"]
				obj["PCI Type"] = card["Type"]
				regex = re.search("^.*(?:SLOT|PCIE)(\d).*",card["Designation"])
				if regex != None:
					obj["PCI Slot"] = regex.group(1)

	return j_connections

def main():
	network = get_network_info()
	result = []
	if network:
		for connection in network:
			result.append(
				{ 
					"connectionName": connection["Connection Name"], 
					"connectionState": connection["Connection State"], 
					"connectionType": connection["Type"], 
					"mac": connection["MAC"], 
					"ipv4": connection["IPv4"] if "IPv4" in connection.keys() else "-", 
					"ipv6": connection["IPv6"] if "IPv6" in connection.keys() else "-", 
					"pciSlot": connection["PCI Slot"] if "PCI Slot" in connection.keys() else "-", 
					"busAddress": connection["Bus Address"] if "Bus Address" in connection.keys() else "-"
				}
			)
		print(json.dumps(result,indent=4))
	else:
		print("Unable to provide network information.")
		print("Please ensure that the following programs are installed:")
		print("\tdmidecode, lshw")
		sys.exit(1)

if __name__ == "__main__":
    main()