#!/usr/bin/env python3
################################################################################
# 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_network_info():
	# run the command: ip addr
	# this will parse the output of this command to obtain the following:
	# 	Connection Name: ex. (eno1)
	# 	Connection State: ex. (DOWN)
	# 	type: ex. (loopback)
	# 	MAC: The MAC address associated with the connection
	# 	inet: the ipv4 address if any
	# 	inet6: the ipv6 address if any
	# This information is manually entered into a .json formatted string
	# called network_json_str. TODO: Modify this to simply create a 
	# dictionary instead of a .json string. 
	try:
		ipaddr_result = subprocess.Popen(
			["ip","addr"],stdout=subprocess.PIPE, universal_newlines=True).stdout
	except:
		return False
	network_json_str = "{\"Network Info\":["
	# search for desired output from the ip addr command and add it to the 
	# json string.
	for line in ipaddr_result:
		regex_connection = re.search("^(\w)\S+\s+(\w+).*state\s(\w+).*",line)
		regex_link = re.search("^\s+link/(\S+)\s(\S+).*",line)
		regex_inet = re.search("^\s+inet\s(\S+).*$",line)
		regex_inet6 = re.search("^\s+inet6\s(\S+).*$",line)
		if regex_connection != None:
			network_json_str += (
					"{\"Connection Name\":\"" + regex_connection.group(2)+"\"," +
					"\"Connection State\":\"" + regex_connection.group(3)+"\"},"
				)
		if regex_link != None:
			network_json_str = network_json_str[:-2]
			network_json_str += (",\"Type\":\""+regex_link.group(1)+"\",")
			network_json_str += ("\"MAC\":\""+regex_link.group(2)+"\"},")
		if regex_inet != None:
			network_json_str = network_json_str[:-2]
			network_json_str += (",\"IPv4\":\""+regex_inet.group(1)+"\"},")
		if regex_inet6 != None:
			network_json_str = network_json_str[:-2]
			network_json_str += (",\"IPv6\":\""+regex_inet6.group(1)+"\"},")

	network_json_str = network_json_str[:-1]
	network_json_str += "]}"

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

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

	# load in the network_json_str as a dictionary
	j_dict = json.loads(network_json_str)

	# add the relevant fields to the list of connections when we have a matching
	# Connection Name
	for obj in j_dict["Network Info"]:
		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(\d).*",card["Designation"])
				if regex != None:
					obj["PCI Slot"] = regex.group(1)

	return json.dumps(j_dict)

def main():
	network = get_network_info()
	if network:
		print(json.dumps(json.loads(network),indent=4))
	else:
		print("Unable to provide network information.")
		print("Please ensure that the following programs are installed:")
		print("\tdmidecode, lshw")

if __name__ == "__main__":
    main()
