Initial version of pldm-visualization-pdr tool
- The pldmtool for GetPDR command lacks to display all PDRs at
once. It fetches only one PDR at a time.
- With a lot of sensors/effecters & with a lot of Host FRU pdrs
coming in due to concurrent maintenance of the system,where
the fru's are added/removed at runtime, it's really necessary
to have a full system view.
- The Intent behind this tool is to fire ssh commands to getPDR
command on BMC and use the obtained json output to parse and to
construct a tabular summary.
- This tool also parses the entity association PDR's and generates
a [picture](https://cdn.discordapp.com/attachments/778790638563885086/850269035827298304/entity_association2021-06-04_123122.pdf)
that explains the entity association hierarchy at any
given point in time.
Signed-off-by: Manojkiran Eda <manojkiran.eda@gmail.com>
Change-Id: I37c05233cff1574c7f49d68a3388c2b4ed3dc2a7
diff --git a/tools/visualize-pdr/pldm_visualise_pdrs.py b/tools/visualize-pdr/pldm_visualise_pdrs.py
new file mode 100755
index 0000000..9672361
--- /dev/null
+++ b/tools/visualize-pdr/pldm_visualise_pdrs.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+
+"""Tool to visualize PLDM PDR's"""
+
+import argparse
+import json
+import hashlib
+import sys
+from datetime import datetime
+import paramiko
+from graphviz import Digraph
+from tabulate import tabulate
+
+
+def connect_to_bmc(hostname, uname, passwd, port):
+
+ """ This function is responsible to connect to the BMC via
+ ssh and returns a client object.
+
+ Parameters:
+ hostname: hostname/IP address of BMC
+ uname: ssh username of BMC
+ passwd: ssh password of BMC
+ port: ssh port of BMC
+
+ """
+
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(hostname, username=uname, password=passwd, port=port)
+ return client
+
+
+def prepare_summary_report(state_sensor_pdr, state_effecter_pdr):
+
+ """ This function is responsible to parse the state sensor pdr
+ and the state effecter pdr dictionaries and creating the
+ summary table.
+
+ Parameters:
+ state_sensor_pdr: list of state sensor pdrs
+ state_effecter_pdr: list of state effecter pdrs
+
+ """
+
+ summary_table = []
+ headers = ["sensor_id", "entity_type", "state_set", "states"]
+ summary_table.append(headers)
+ for value in state_sensor_pdr.values():
+ summary_record = []
+ sensor_possible_states = ''
+ for sensor_state in value["possibleStates[0]"]:
+ sensor_possible_states += sensor_state+"\n"
+ summary_record.extend([value["sensorID"], value["entityType"],
+ value["stateSetID[0]"],
+ sensor_possible_states])
+ summary_table.append(summary_record)
+ print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+ print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
+
+ summary_table = []
+ headers = ["effecter_id", "entity_type", "state_set", "states"]
+ summary_table.append(headers)
+ for value in state_effecter_pdr.values():
+ summary_record = []
+ effecter_possible_states = ''
+ for state in value["possibleStates[0]"]:
+ effecter_possible_states += state+"\n"
+ summary_record.extend([value["effecterID"], value["entityType"],
+ value["stateSetID[0]"],
+ effecter_possible_states])
+ summary_table.append(summary_record)
+ print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
+
+
+def draw_entity_associations(pdr, counter):
+
+ """ This function is responsible to create a picture that captures
+ the entity association hierarchy based on the entity association
+ PDR's received from the BMC.
+
+ Parameters:
+ pdr: list of entity association PDR's
+ counter: variable to capture the count of PDR's to unflatten
+ the tree
+
+ """
+
+ dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1',
+ 'style': 'filled'})
+ dot.attr(label=r'\n\nEntity Relation Diagram < ' +
+ str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n')
+ dot.attr(fontsize='20')
+ edge_list = []
+ for value in pdr.values():
+ parentnode = str(value["containerEntityType"]) + \
+ str(value["containerEntityInstanceNumber"])
+ dot.node(hashlib.md5((parentnode +
+ str(value["containerEntityContainerID"]))
+ .encode()).hexdigest(), parentnode)
+
+ for i in range(1, value["containedEntityCount"]+1):
+ childnode = str(value[f"containedEntityType[{i}]"]) + \
+ str(value[f"containedEntityInstanceNumber[{i}]"])
+ cid = str(value[f"containedEntityContainerID[{i}]"])
+ dot.node(hashlib.md5((childnode + cid)
+ .encode()).hexdigest(), childnode)
+
+ if[hashlib.md5((parentnode +
+ str(value["containerEntityContainerID"]))
+ .encode()).hexdigest(),
+ hashlib.md5((childnode + cid)
+ .encode()).hexdigest()] not in edge_list:
+ edge_list.append([hashlib.md5((parentnode +
+ str(value["containerEntityContainerID"]))
+ .encode()).hexdigest(),
+ hashlib.md5((childnode + cid)
+ .encode()).hexdigest()])
+ dot.edge(hashlib.md5((parentnode +
+ str(value["containerEntityContainerID"]))
+ .encode()).hexdigest(),
+ hashlib.md5((childnode + cid).encode()).hexdigest())
+ unflattentree = dot.unflatten(stagger=(round(counter/3)))
+ unflattentree.render(filename='entity_association_' +
+ str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")),
+ view=False, cleanup=True, format='pdf')
+
+
+def fetch_pdrs_from_bmc(client):
+
+ """ This is the core function that would use the existing ssh connection
+ object to connect to BMC and fire the getPDR pldmtool command
+ and it then agreegates the data received from all the calls into
+ the respective dictionaries based on the PDR Type.
+
+ Parameters:
+ client: paramiko ssh client object
+
+ """
+
+ entity_association_pdr = {}
+ state_sensor_pdr = {}
+ state_effecter_pdr = {}
+ state_effecter_pdr = {}
+ numeric_pdr = {}
+ fru_record_set_pdr = {}
+ tl_pdr = {}
+ handle_number = 0
+ while True:
+ my_str = ''
+ command = 'pldmtool platform getpdr -d ' + str(handle_number)
+ output = client.exec_command(command)
+ for line in output[1]:
+ my_str += line.strip('\n')
+ my_dic = json.loads(my_str)
+ sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
+ sys.stdout.flush()
+ if my_dic["PDRType"] == "Entity Association PDR":
+ entity_association_pdr[handle_number] = my_dic
+ if my_dic["PDRType"] == "State Sensor PDR":
+ state_sensor_pdr[handle_number] = my_dic
+ if my_dic["PDRType"] == "State Effecter PDR":
+ state_effecter_pdr[handle_number] = my_dic
+ if my_dic["PDRType"] == "FRU Record Set PDR":
+ fru_record_set_pdr[handle_number] = my_dic
+ if my_dic["PDRType"] == "Terminus Locator PDR":
+ tl_pdr[handle_number] = my_dic
+ if my_dic["PDRType"] == "Numeric Effecter PDR":
+ numeric_pdr[handle_number] = my_dic
+ if not my_dic["nextRecordHandle"] == 0:
+ handle_number = my_dic["nextRecordHandle"]
+ else:
+ break
+ client.close()
+
+ total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
+ len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
+ len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
+ print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
+ print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
+ print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
+ print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
+ print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
+ print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
+ print("Number of Entity Association PDR's : ",
+ len(entity_association_pdr.keys()))
+ return (entity_association_pdr, state_sensor_pdr,
+ state_effecter_pdr, len(fru_record_set_pdr.keys()))
+
+
+def main():
+
+ """ Create a summary table capturing the information of all the PDR's
+ from the BMC & also create a diagram that captures the entity
+ association hierarchy."""
+
+ parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
+ parser.add_argument('--bmc', type=str, required=True,
+ help="BMC IPAddress/BMC Hostname")
+ parser.add_argument('--user', type=str, required=True,
+ help="BMC username")
+ parser.add_argument('--password', type=str, required=True,
+ help="BMC Password")
+ parser.add_argument('--port', type=int, help="BMC SSH port",
+ default=22)
+ args = parser.parse_args()
+ if args.bmc and args.password and args.user:
+ client = connect_to_bmc(args.bmc, args.user, args.password, args.port)
+ association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
+ fetch_pdrs_from_bmc(client)
+ draw_entity_associations(association_pdr, counter)
+ prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
+
+
+if __name__ == "__main__":
+ main()