1#!/usr/bin/env python3 2 3"""Tool to visualize PLDM PDR's""" 4 5import argparse 6import json 7import hashlib 8import sys 9from datetime import datetime 10import paramiko 11from graphviz import Digraph 12from tabulate import tabulate 13 14 15def connect_to_bmc(hostname, uname, passwd, port): 16 17 """ This function is responsible to connect to the BMC via 18 ssh and returns a client object. 19 20 Parameters: 21 hostname: hostname/IP address of BMC 22 uname: ssh username of BMC 23 passwd: ssh password of BMC 24 port: ssh port of BMC 25 26 """ 27 28 client = paramiko.SSHClient() 29 client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 30 client.connect(hostname, username=uname, password=passwd, port=port) 31 return client 32 33 34def prepare_summary_report(state_sensor_pdr, state_effecter_pdr): 35 36 """ This function is responsible to parse the state sensor pdr 37 and the state effecter pdr dictionaries and creating the 38 summary table. 39 40 Parameters: 41 state_sensor_pdr: list of state sensor pdrs 42 state_effecter_pdr: list of state effecter pdrs 43 44 """ 45 46 summary_table = [] 47 headers = ["sensor_id", "entity_type", "state_set", "states"] 48 summary_table.append(headers) 49 for value in state_sensor_pdr.values(): 50 summary_record = [] 51 sensor_possible_states = '' 52 for sensor_state in value["possibleStates[0]"]: 53 sensor_possible_states += sensor_state+"\n" 54 summary_record.extend([value["sensorID"], value["entityType"], 55 value["stateSetID[0]"], 56 sensor_possible_states]) 57 summary_table.append(summary_record) 58 print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S")) 59 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow")) 60 61 summary_table = [] 62 headers = ["effecter_id", "entity_type", "state_set", "states"] 63 summary_table.append(headers) 64 for value in state_effecter_pdr.values(): 65 summary_record = [] 66 effecter_possible_states = '' 67 for state in value["possibleStates[0]"]: 68 effecter_possible_states += state+"\n" 69 summary_record.extend([value["effecterID"], value["entityType"], 70 value["stateSetID[0]"], 71 effecter_possible_states]) 72 summary_table.append(summary_record) 73 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow")) 74 75 76def draw_entity_associations(pdr, counter): 77 78 """ This function is responsible to create a picture that captures 79 the entity association hierarchy based on the entity association 80 PDR's received from the BMC. 81 82 Parameters: 83 pdr: list of entity association PDR's 84 counter: variable to capture the count of PDR's to unflatten 85 the tree 86 87 """ 88 89 dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1', 90 'style': 'filled'}) 91 dot.attr(label=r'\n\nEntity Relation Diagram < ' + 92 str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n') 93 dot.attr(fontsize='20') 94 edge_list = [] 95 for value in pdr.values(): 96 parentnode = str(value["containerEntityType"]) + \ 97 str(value["containerEntityInstanceNumber"]) 98 dot.node(hashlib.md5((parentnode + 99 str(value["containerEntityContainerID"])) 100 .encode()).hexdigest(), parentnode) 101 102 for i in range(1, value["containedEntityCount"]+1): 103 childnode = str(value[f"containedEntityType[{i}]"]) + \ 104 str(value[f"containedEntityInstanceNumber[{i}]"]) 105 cid = str(value[f"containedEntityContainerID[{i}]"]) 106 dot.node(hashlib.md5((childnode + cid) 107 .encode()).hexdigest(), childnode) 108 109 if[hashlib.md5((parentnode + 110 str(value["containerEntityContainerID"])) 111 .encode()).hexdigest(), 112 hashlib.md5((childnode + cid) 113 .encode()).hexdigest()] not in edge_list: 114 edge_list.append([hashlib.md5((parentnode + 115 str(value["containerEntityContainerID"])) 116 .encode()).hexdigest(), 117 hashlib.md5((childnode + cid) 118 .encode()).hexdigest()]) 119 dot.edge(hashlib.md5((parentnode + 120 str(value["containerEntityContainerID"])) 121 .encode()).hexdigest(), 122 hashlib.md5((childnode + cid).encode()).hexdigest()) 123 unflattentree = dot.unflatten(stagger=(round(counter/3))) 124 unflattentree.render(filename='entity_association_' + 125 str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")), 126 view=False, cleanup=True, format='pdf') 127 128 129class PLDMToolError(Exception): 130 """ Exception class intended to be used to hold pldmtool invocation failure 131 information such as exit status and stderr. 132 133 """ 134 135 def __init__(self, status, stderr): 136 msg = "pldmtool failed with exit status {}.\n".format(status) 137 msg += "stderr: \n\n{}".format(stderr) 138 super(PLDMToolError, self).__init__(msg) 139 140 141def process_pldmtool_output(stdout_channel, stderr_channel): 142 """ Ensure pldmtool runs without error and if it does fail, detect that and 143 show the pldmtool exit status and it's stderr. 144 145 Parameters: 146 stdout_channel: file-like stdout channel 147 stderr_channel: file-like stderr channel 148 149 """ 150 151 status = stderr_channel.channel.recv_exit_status() 152 if status == 0: 153 return json.load(stdout_channel) 154 155 raise PLDMToolError(status, "".join(stderr_channel)) 156 157 158def get_pdrs(client): 159 """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each 160 record in the PDR repository. 161 162 Parameters: 163 client: paramiko ssh client object 164 165 """ 166 167 command_fmt = 'pldmtool platform getpdr -d {}' 168 record_handle = 0 169 while True: 170 output = client.exec_command(command_fmt.format(str(record_handle))) 171 _, stdout, stderr = output 172 pdr = process_pldmtool_output(stdout, stderr) 173 yield record_handle, pdr 174 record_handle = pdr["nextRecordHandle"] 175 if record_handle == 0: 176 break 177 178 179def fetch_pdrs_from_bmc(client): 180 181 """ This is the core function that would use the existing ssh connection 182 object to connect to BMC and fire the getPDR pldmtool command 183 and it then agreegates the data received from all the calls into 184 the respective dictionaries based on the PDR Type. 185 186 Parameters: 187 client: paramiko ssh client object 188 189 """ 190 191 entity_association_pdr = {} 192 state_sensor_pdr = {} 193 state_effecter_pdr = {} 194 state_effecter_pdr = {} 195 numeric_pdr = {} 196 fru_record_set_pdr = {} 197 tl_pdr = {} 198 for handle_number, my_dic in get_pdrs(client): 199 sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number)) 200 sys.stdout.flush() 201 if my_dic["PDRType"] == "Entity Association PDR": 202 entity_association_pdr[handle_number] = my_dic 203 if my_dic["PDRType"] == "State Sensor PDR": 204 state_sensor_pdr[handle_number] = my_dic 205 if my_dic["PDRType"] == "State Effecter PDR": 206 state_effecter_pdr[handle_number] = my_dic 207 if my_dic["PDRType"] == "FRU Record Set PDR": 208 fru_record_set_pdr[handle_number] = my_dic 209 if my_dic["PDRType"] == "Terminus Locator PDR": 210 tl_pdr[handle_number] = my_dic 211 if my_dic["PDRType"] == "Numeric Effecter PDR": 212 numeric_pdr[handle_number] = my_dic 213 client.close() 214 215 total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \ 216 len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \ 217 len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys()) 218 print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s") 219 print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys())) 220 print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys())) 221 print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys())) 222 print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys())) 223 print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys())) 224 print("Number of Entity Association PDR's : ", 225 len(entity_association_pdr.keys())) 226 return (entity_association_pdr, state_sensor_pdr, 227 state_effecter_pdr, len(fru_record_set_pdr.keys())) 228 229 230def main(): 231 232 """ Create a summary table capturing the information of all the PDR's 233 from the BMC & also create a diagram that captures the entity 234 association hierarchy.""" 235 236 parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py') 237 parser.add_argument('--bmc', type=str, required=True, 238 help="BMC IPAddress/BMC Hostname") 239 parser.add_argument('--user', type=str, help="BMC username") 240 parser.add_argument('--password', type=str, required=True, 241 help="BMC Password") 242 parser.add_argument('--port', type=int, help="BMC SSH port", 243 default=22) 244 args = parser.parse_args() 245 client = connect_to_bmc(args.bmc, args.user, args.password, args.port) 246 association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \ 247 fetch_pdrs_from_bmc(client) 248 draw_entity_associations(association_pdr, counter) 249 prepare_summary_report(state_sensor_pdr, state_effecter_pdr) 250 251 252if __name__ == "__main__": 253 main() 254