1b8cc3257SManojkiran Eda#!/usr/bin/env python3 2b8cc3257SManojkiran Eda 3b8cc3257SManojkiran Eda"""Tool to visualize PLDM PDR's""" 4b8cc3257SManojkiran Eda 5b8cc3257SManojkiran Edaimport argparse 6b8cc3257SManojkiran Edaimport json 7b8cc3257SManojkiran Edaimport hashlib 8b8cc3257SManojkiran Edaimport sys 9b8cc3257SManojkiran Edafrom datetime import datetime 10b8cc3257SManojkiran Edaimport paramiko 11b8cc3257SManojkiran Edafrom graphviz import Digraph 12b8cc3257SManojkiran Edafrom tabulate import tabulate 13b8cc3257SManojkiran Eda 14b8cc3257SManojkiran Eda 15b8cc3257SManojkiran Edadef connect_to_bmc(hostname, uname, passwd, port): 16b8cc3257SManojkiran Eda 17b8cc3257SManojkiran Eda """ This function is responsible to connect to the BMC via 18b8cc3257SManojkiran Eda ssh and returns a client object. 19b8cc3257SManojkiran Eda 20b8cc3257SManojkiran Eda Parameters: 21b8cc3257SManojkiran Eda hostname: hostname/IP address of BMC 22b8cc3257SManojkiran Eda uname: ssh username of BMC 23b8cc3257SManojkiran Eda passwd: ssh password of BMC 24b8cc3257SManojkiran Eda port: ssh port of BMC 25b8cc3257SManojkiran Eda 26b8cc3257SManojkiran Eda """ 27b8cc3257SManojkiran Eda 28b8cc3257SManojkiran Eda client = paramiko.SSHClient() 29b8cc3257SManojkiran Eda client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 30b8cc3257SManojkiran Eda client.connect(hostname, username=uname, password=passwd, port=port) 31b8cc3257SManojkiran Eda return client 32b8cc3257SManojkiran Eda 33b8cc3257SManojkiran Eda 34b8cc3257SManojkiran Edadef prepare_summary_report(state_sensor_pdr, state_effecter_pdr): 35b8cc3257SManojkiran Eda 36b8cc3257SManojkiran Eda """ This function is responsible to parse the state sensor pdr 37b8cc3257SManojkiran Eda and the state effecter pdr dictionaries and creating the 38b8cc3257SManojkiran Eda summary table. 39b8cc3257SManojkiran Eda 40b8cc3257SManojkiran Eda Parameters: 41b8cc3257SManojkiran Eda state_sensor_pdr: list of state sensor pdrs 42b8cc3257SManojkiran Eda state_effecter_pdr: list of state effecter pdrs 43b8cc3257SManojkiran Eda 44b8cc3257SManojkiran Eda """ 45b8cc3257SManojkiran Eda 46b8cc3257SManojkiran Eda summary_table = [] 47b8cc3257SManojkiran Eda headers = ["sensor_id", "entity_type", "state_set", "states"] 48b8cc3257SManojkiran Eda summary_table.append(headers) 49b8cc3257SManojkiran Eda for value in state_sensor_pdr.values(): 50b8cc3257SManojkiran Eda summary_record = [] 51b8cc3257SManojkiran Eda sensor_possible_states = '' 52b8cc3257SManojkiran Eda for sensor_state in value["possibleStates[0]"]: 53b8cc3257SManojkiran Eda sensor_possible_states += sensor_state+"\n" 54b8cc3257SManojkiran Eda summary_record.extend([value["sensorID"], value["entityType"], 55b8cc3257SManojkiran Eda value["stateSetID[0]"], 56b8cc3257SManojkiran Eda sensor_possible_states]) 57b8cc3257SManojkiran Eda summary_table.append(summary_record) 58b8cc3257SManojkiran Eda print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 59b8cc3257SManojkiran Eda print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow")) 60b8cc3257SManojkiran Eda 61b8cc3257SManojkiran Eda summary_table = [] 62b8cc3257SManojkiran Eda headers = ["effecter_id", "entity_type", "state_set", "states"] 63b8cc3257SManojkiran Eda summary_table.append(headers) 64b8cc3257SManojkiran Eda for value in state_effecter_pdr.values(): 65b8cc3257SManojkiran Eda summary_record = [] 66b8cc3257SManojkiran Eda effecter_possible_states = '' 67b8cc3257SManojkiran Eda for state in value["possibleStates[0]"]: 68b8cc3257SManojkiran Eda effecter_possible_states += state+"\n" 69b8cc3257SManojkiran Eda summary_record.extend([value["effecterID"], value["entityType"], 70b8cc3257SManojkiran Eda value["stateSetID[0]"], 71b8cc3257SManojkiran Eda effecter_possible_states]) 72b8cc3257SManojkiran Eda summary_table.append(summary_record) 73b8cc3257SManojkiran Eda print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow")) 74b8cc3257SManojkiran Eda 75b8cc3257SManojkiran Eda 76b8cc3257SManojkiran Edadef draw_entity_associations(pdr, counter): 77b8cc3257SManojkiran Eda 78b8cc3257SManojkiran Eda """ This function is responsible to create a picture that captures 79b8cc3257SManojkiran Eda the entity association hierarchy based on the entity association 80b8cc3257SManojkiran Eda PDR's received from the BMC. 81b8cc3257SManojkiran Eda 82b8cc3257SManojkiran Eda Parameters: 83b8cc3257SManojkiran Eda pdr: list of entity association PDR's 84b8cc3257SManojkiran Eda counter: variable to capture the count of PDR's to unflatten 85b8cc3257SManojkiran Eda the tree 86b8cc3257SManojkiran Eda 87b8cc3257SManojkiran Eda """ 88b8cc3257SManojkiran Eda 89b8cc3257SManojkiran Eda dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1', 90b8cc3257SManojkiran Eda 'style': 'filled'}) 91b8cc3257SManojkiran Eda dot.attr(label=r'\n\nEntity Relation Diagram < ' + 92b8cc3257SManojkiran Eda str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n') 93b8cc3257SManojkiran Eda dot.attr(fontsize='20') 94b8cc3257SManojkiran Eda edge_list = [] 95b8cc3257SManojkiran Eda for value in pdr.values(): 96b8cc3257SManojkiran Eda parentnode = str(value["containerEntityType"]) + \ 97b8cc3257SManojkiran Eda str(value["containerEntityInstanceNumber"]) 98b8cc3257SManojkiran Eda dot.node(hashlib.md5((parentnode + 99b8cc3257SManojkiran Eda str(value["containerEntityContainerID"])) 100b8cc3257SManojkiran Eda .encode()).hexdigest(), parentnode) 101b8cc3257SManojkiran Eda 102b8cc3257SManojkiran Eda for i in range(1, value["containedEntityCount"]+1): 103b8cc3257SManojkiran Eda childnode = str(value[f"containedEntityType[{i}]"]) + \ 104b8cc3257SManojkiran Eda str(value[f"containedEntityInstanceNumber[{i}]"]) 105b8cc3257SManojkiran Eda cid = str(value[f"containedEntityContainerID[{i}]"]) 106b8cc3257SManojkiran Eda dot.node(hashlib.md5((childnode + cid) 107b8cc3257SManojkiran Eda .encode()).hexdigest(), childnode) 108b8cc3257SManojkiran Eda 109b8cc3257SManojkiran Eda if[hashlib.md5((parentnode + 110b8cc3257SManojkiran Eda str(value["containerEntityContainerID"])) 111b8cc3257SManojkiran Eda .encode()).hexdigest(), 112b8cc3257SManojkiran Eda hashlib.md5((childnode + cid) 113b8cc3257SManojkiran Eda .encode()).hexdigest()] not in edge_list: 114b8cc3257SManojkiran Eda edge_list.append([hashlib.md5((parentnode + 115b8cc3257SManojkiran Eda str(value["containerEntityContainerID"])) 116b8cc3257SManojkiran Eda .encode()).hexdigest(), 117b8cc3257SManojkiran Eda hashlib.md5((childnode + cid) 118b8cc3257SManojkiran Eda .encode()).hexdigest()]) 119b8cc3257SManojkiran Eda dot.edge(hashlib.md5((parentnode + 120b8cc3257SManojkiran Eda str(value["containerEntityContainerID"])) 121b8cc3257SManojkiran Eda .encode()).hexdigest(), 122b8cc3257SManojkiran Eda hashlib.md5((childnode + cid).encode()).hexdigest()) 123b8cc3257SManojkiran Eda unflattentree = dot.unflatten(stagger=(round(counter/3))) 124b8cc3257SManojkiran Eda unflattentree.render(filename='entity_association_' + 125b8cc3257SManojkiran Eda str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")), 126b8cc3257SManojkiran Eda view=False, cleanup=True, format='pdf') 127b8cc3257SManojkiran Eda 128b8cc3257SManojkiran Eda 129260f75a6SBrad Bishopclass PLDMToolError(Exception): 130260f75a6SBrad Bishop """ Exception class intended to be used to hold pldmtool invocation failure 131260f75a6SBrad Bishop information such as exit status and stderr. 132260f75a6SBrad Bishop 133260f75a6SBrad Bishop """ 134260f75a6SBrad Bishop 135260f75a6SBrad Bishop def __init__(self, status, stderr): 136260f75a6SBrad Bishop msg = "pldmtool failed with exit status {}.\n".format(status) 137260f75a6SBrad Bishop msg += "stderr: \n\n{}".format(stderr) 138260f75a6SBrad Bishop super(PLDMToolError, self).__init__(msg) 139*98cb3efbSBrad Bishop self.status = status 140*98cb3efbSBrad Bishop 141*98cb3efbSBrad Bishop def get_status(self): 142*98cb3efbSBrad Bishop return self.status 143260f75a6SBrad Bishop 144260f75a6SBrad Bishop 145260f75a6SBrad Bishopdef process_pldmtool_output(stdout_channel, stderr_channel): 146260f75a6SBrad Bishop """ Ensure pldmtool runs without error and if it does fail, detect that and 147260f75a6SBrad Bishop show the pldmtool exit status and it's stderr. 148260f75a6SBrad Bishop 149*98cb3efbSBrad Bishop A simpler implementation would just wait for the pldmtool exit status 150*98cb3efbSBrad Bishop prior to attempting to decode it's stdout. Instead, optimize for the 151*98cb3efbSBrad Bishop no error case and allow the json decoder to consume pldmtool stdout as 152*98cb3efbSBrad Bishop soon as it is available (in parallel). This results in the following 153*98cb3efbSBrad Bishop error scenarios: 154*98cb3efbSBrad Bishop - pldmtool fails and the decoder fails 155*98cb3efbSBrad Bishop Ignore the decoder fail and throw PLDMToolError. 156*98cb3efbSBrad Bishop - pldmtool fails and the decoder doesn't fail 157*98cb3efbSBrad Bishop Throw PLDMToolError. 158*98cb3efbSBrad Bishop - pldmtool doesn't fail and the decoder does fail 159*98cb3efbSBrad Bishop This is a pldmtool bug - re-throw the decoder error. 160*98cb3efbSBrad Bishop 161260f75a6SBrad Bishop Parameters: 162260f75a6SBrad Bishop stdout_channel: file-like stdout channel 163260f75a6SBrad Bishop stderr_channel: file-like stderr channel 164260f75a6SBrad Bishop 165260f75a6SBrad Bishop """ 166260f75a6SBrad Bishop 167*98cb3efbSBrad Bishop status = 0 168*98cb3efbSBrad Bishop try: 169*98cb3efbSBrad Bishop data = json.load(stdout_channel) 170*98cb3efbSBrad Bishop # it's unlikely, but possible, that pldmtool failed but still wrote a 171*98cb3efbSBrad Bishop # valid json document - so check for that. 172260f75a6SBrad Bishop status = stderr_channel.channel.recv_exit_status() 173260f75a6SBrad Bishop if status == 0: 174*98cb3efbSBrad Bishop return data 175*98cb3efbSBrad Bishop except json.decoder.JSONDecodeError: 176*98cb3efbSBrad Bishop # pldmtool wrote an invalid json document. Check to see if it had 177*98cb3efbSBrad Bishop # non-zero exit status. 178*98cb3efbSBrad Bishop status = stderr_channel.channel.recv_exit_status() 179*98cb3efbSBrad Bishop if status == 0: 180*98cb3efbSBrad Bishop # pldmtool didn't have non zero exit status, so it wrote an invalid 181*98cb3efbSBrad Bishop # json document and the JSONDecodeError is the correct error. 182*98cb3efbSBrad Bishop raise 183260f75a6SBrad Bishop 184*98cb3efbSBrad Bishop # pldmtool had a non-zero exit status, so throw an error for that, possibly 185*98cb3efbSBrad Bishop # discarding a spurious JSONDecodeError exception. 186260f75a6SBrad Bishop raise PLDMToolError(status, "".join(stderr_channel)) 187260f75a6SBrad Bishop 188260f75a6SBrad Bishop 189*98cb3efbSBrad Bishopdef get_pdrs_one_at_a_time(client): 19091523137SBrad Bishop """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each 19191523137SBrad Bishop record in the PDR repository. 19291523137SBrad Bishop 19391523137SBrad Bishop Parameters: 19491523137SBrad Bishop client: paramiko ssh client object 19591523137SBrad Bishop 19691523137SBrad Bishop """ 19791523137SBrad Bishop 19891523137SBrad Bishop command_fmt = 'pldmtool platform getpdr -d {}' 19991523137SBrad Bishop record_handle = 0 20091523137SBrad Bishop while True: 20191523137SBrad Bishop output = client.exec_command(command_fmt.format(str(record_handle))) 20291523137SBrad Bishop _, stdout, stderr = output 203260f75a6SBrad Bishop pdr = process_pldmtool_output(stdout, stderr) 20491523137SBrad Bishop yield record_handle, pdr 20591523137SBrad Bishop record_handle = pdr["nextRecordHandle"] 20691523137SBrad Bishop if record_handle == 0: 20791523137SBrad Bishop break 20891523137SBrad Bishop 20991523137SBrad Bishop 210*98cb3efbSBrad Bishopdef get_all_pdrs_at_once(client): 211*98cb3efbSBrad Bishop """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each 212*98cb3efbSBrad Bishop record in the PDR repository. Use pldmtool platform getpdr --all. 213*98cb3efbSBrad Bishop 214*98cb3efbSBrad Bishop Parameters: 215*98cb3efbSBrad Bishop client: paramiko ssh client object 216*98cb3efbSBrad Bishop 217*98cb3efbSBrad Bishop """ 218*98cb3efbSBrad Bishop 219*98cb3efbSBrad Bishop _, stdout, stderr = client.exec_command('pldmtool platform getpdr -a') 220*98cb3efbSBrad Bishop all_pdrs = process_pldmtool_output(stdout, stderr) 221*98cb3efbSBrad Bishop 222*98cb3efbSBrad Bishop # Explicitly request record 0 to find out what the real first record is. 223*98cb3efbSBrad Bishop _, stdout, stderr = client.exec_command('pldmtool platform getpdr -d 0') 224*98cb3efbSBrad Bishop pdr_0 = process_pldmtool_output(stdout, stderr) 225*98cb3efbSBrad Bishop record_handle = pdr_0["recordHandle"] 226*98cb3efbSBrad Bishop 227*98cb3efbSBrad Bishop while True: 228*98cb3efbSBrad Bishop for pdr in all_pdrs: 229*98cb3efbSBrad Bishop if pdr["recordHandle"] == record_handle: 230*98cb3efbSBrad Bishop yield record_handle, pdr 231*98cb3efbSBrad Bishop record_handle = pdr["nextRecordHandle"] 232*98cb3efbSBrad Bishop if record_handle == 0: 233*98cb3efbSBrad Bishop return 234*98cb3efbSBrad Bishop raise RuntimeError( 235*98cb3efbSBrad Bishop "Dangling reference to record {}".format(record_handle)) 236*98cb3efbSBrad Bishop 237*98cb3efbSBrad Bishop 238*98cb3efbSBrad Bishopdef get_pdrs(client): 239*98cb3efbSBrad Bishop """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each 240*98cb3efbSBrad Bishop record in the PDR repository. Use pldmtool platform getpdr --all or 241*98cb3efbSBrad Bishop fallback on getting them one at a time if pldmtool doesn't support the 242*98cb3efbSBrad Bishop --all option. 243*98cb3efbSBrad Bishop 244*98cb3efbSBrad Bishop Parameters: 245*98cb3efbSBrad Bishop client: paramiko ssh client object 246*98cb3efbSBrad Bishop 247*98cb3efbSBrad Bishop """ 248*98cb3efbSBrad Bishop try: 249*98cb3efbSBrad Bishop for record_handle, pdr in get_all_pdrs_at_once(client): 250*98cb3efbSBrad Bishop yield record_handle, pdr 251*98cb3efbSBrad Bishop return 252*98cb3efbSBrad Bishop except PLDMToolError as e: 253*98cb3efbSBrad Bishop # No support for the -a option 254*98cb3efbSBrad Bishop if e.get_status() != 106: 255*98cb3efbSBrad Bishop raise 256*98cb3efbSBrad Bishop except json.decoder.JSONDecodeError as e: 257*98cb3efbSBrad Bishop # Some versions of pldmtool don't print valid json documents with -a 258*98cb3efbSBrad Bishop if e.msg != "Extra data": 259*98cb3efbSBrad Bishop raise 260*98cb3efbSBrad Bishop 261*98cb3efbSBrad Bishop for record_handle, pdr in get_pdrs_one_at_a_time(client): 262*98cb3efbSBrad Bishop yield record_handle, pdr 263*98cb3efbSBrad Bishop 264*98cb3efbSBrad Bishop 265b8cc3257SManojkiran Edadef fetch_pdrs_from_bmc(client): 266b8cc3257SManojkiran Eda 267b8cc3257SManojkiran Eda """ This is the core function that would use the existing ssh connection 268b8cc3257SManojkiran Eda object to connect to BMC and fire the getPDR pldmtool command 269b8cc3257SManojkiran Eda and it then agreegates the data received from all the calls into 270b8cc3257SManojkiran Eda the respective dictionaries based on the PDR Type. 271b8cc3257SManojkiran Eda 272b8cc3257SManojkiran Eda Parameters: 273b8cc3257SManojkiran Eda client: paramiko ssh client object 274b8cc3257SManojkiran Eda 275b8cc3257SManojkiran Eda """ 276b8cc3257SManojkiran Eda 277b8cc3257SManojkiran Eda entity_association_pdr = {} 278b8cc3257SManojkiran Eda state_sensor_pdr = {} 279b8cc3257SManojkiran Eda state_effecter_pdr = {} 280b8cc3257SManojkiran Eda state_effecter_pdr = {} 281b8cc3257SManojkiran Eda numeric_pdr = {} 282b8cc3257SManojkiran Eda fru_record_set_pdr = {} 283b8cc3257SManojkiran Eda tl_pdr = {} 28491523137SBrad Bishop for handle_number, my_dic in get_pdrs(client): 285b8cc3257SManojkiran Eda sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number)) 286b8cc3257SManojkiran Eda sys.stdout.flush() 287b8cc3257SManojkiran Eda if my_dic["PDRType"] == "Entity Association PDR": 288b8cc3257SManojkiran Eda entity_association_pdr[handle_number] = my_dic 289b8cc3257SManojkiran Eda if my_dic["PDRType"] == "State Sensor PDR": 290b8cc3257SManojkiran Eda state_sensor_pdr[handle_number] = my_dic 291b8cc3257SManojkiran Eda if my_dic["PDRType"] == "State Effecter PDR": 292b8cc3257SManojkiran Eda state_effecter_pdr[handle_number] = my_dic 293b8cc3257SManojkiran Eda if my_dic["PDRType"] == "FRU Record Set PDR": 294b8cc3257SManojkiran Eda fru_record_set_pdr[handle_number] = my_dic 295b8cc3257SManojkiran Eda if my_dic["PDRType"] == "Terminus Locator PDR": 296b8cc3257SManojkiran Eda tl_pdr[handle_number] = my_dic 297b8cc3257SManojkiran Eda if my_dic["PDRType"] == "Numeric Effecter PDR": 298b8cc3257SManojkiran Eda numeric_pdr[handle_number] = my_dic 299b8cc3257SManojkiran Eda client.close() 300b8cc3257SManojkiran Eda 301b8cc3257SManojkiran Eda total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \ 302b8cc3257SManojkiran Eda len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \ 303b8cc3257SManojkiran Eda len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys()) 304b8cc3257SManojkiran Eda print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s") 305b8cc3257SManojkiran Eda print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys())) 306b8cc3257SManojkiran Eda print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys())) 307b8cc3257SManojkiran Eda print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys())) 308b8cc3257SManojkiran Eda print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys())) 309b8cc3257SManojkiran Eda print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys())) 310b8cc3257SManojkiran Eda print("Number of Entity Association PDR's : ", 311b8cc3257SManojkiran Eda len(entity_association_pdr.keys())) 312b8cc3257SManojkiran Eda return (entity_association_pdr, state_sensor_pdr, 313b8cc3257SManojkiran Eda state_effecter_pdr, len(fru_record_set_pdr.keys())) 314b8cc3257SManojkiran Eda 315b8cc3257SManojkiran Eda 316b8cc3257SManojkiran Edadef main(): 317b8cc3257SManojkiran Eda 318b8cc3257SManojkiran Eda """ Create a summary table capturing the information of all the PDR's 319b8cc3257SManojkiran Eda from the BMC & also create a diagram that captures the entity 320b8cc3257SManojkiran Eda association hierarchy.""" 321b8cc3257SManojkiran Eda 322b8cc3257SManojkiran Eda parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py') 323b8cc3257SManojkiran Eda parser.add_argument('--bmc', type=str, required=True, 324b8cc3257SManojkiran Eda help="BMC IPAddress/BMC Hostname") 325e0c55c8aSBrad Bishop parser.add_argument('--user', type=str, help="BMC username") 326b8cc3257SManojkiran Eda parser.add_argument('--password', type=str, required=True, 327b8cc3257SManojkiran Eda help="BMC Password") 328b8cc3257SManojkiran Eda parser.add_argument('--port', type=int, help="BMC SSH port", 329b8cc3257SManojkiran Eda default=22) 330b8cc3257SManojkiran Eda args = parser.parse_args() 331b8cc3257SManojkiran Eda client = connect_to_bmc(args.bmc, args.user, args.password, args.port) 332b8cc3257SManojkiran Eda association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \ 333b8cc3257SManojkiran Eda fetch_pdrs_from_bmc(client) 334b8cc3257SManojkiran Eda draw_entity_associations(association_pdr, counter) 335b8cc3257SManojkiran Eda prepare_summary_report(state_sensor_pdr, state_effecter_pdr) 336b8cc3257SManojkiran Eda 337b8cc3257SManojkiran Eda 338b8cc3257SManojkiran Edaif __name__ == "__main__": 339b8cc3257SManojkiran Eda main() 340