| Manojkiran Eda | b8cc325 | 2021-05-24 11:29:36 +0530 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
 | 2 |  | 
 | 3 | """Tool to visualize PLDM PDR's""" | 
 | 4 |  | 
 | 5 | import argparse | 
 | 6 | import json | 
 | 7 | import hashlib | 
 | 8 | import sys | 
 | 9 | from datetime import datetime | 
 | 10 | import paramiko | 
 | 11 | from graphviz import Digraph | 
 | 12 | from tabulate import tabulate | 
 | 13 |  | 
 | 14 |  | 
 | 15 | def 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 |  | 
 | 34 | def 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 |  | 
 | 76 | def 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 |  | 
| Brad Bishop | 9152313 | 2021-10-14 19:25:37 -0400 | [diff] [blame] | 129 | def get_pdrs(client): | 
 | 130 |     """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each | 
 | 131 |         record in the PDR repository. | 
 | 132 |  | 
 | 133 |         Parameters: | 
 | 134 |             client: paramiko ssh client object | 
 | 135 |  | 
 | 136 |     """ | 
 | 137 |  | 
 | 138 |     command_fmt = 'pldmtool platform getpdr -d {}' | 
 | 139 |     record_handle = 0 | 
 | 140 |     while True: | 
 | 141 |         output = client.exec_command(command_fmt.format(str(record_handle))) | 
 | 142 |         _, stdout, stderr = output | 
 | 143 |         pdr = json.load(stdout) | 
 | 144 |         yield record_handle, pdr | 
 | 145 |         record_handle = pdr["nextRecordHandle"] | 
 | 146 |         if record_handle == 0: | 
 | 147 |             break | 
 | 148 |  | 
 | 149 |  | 
| Manojkiran Eda | b8cc325 | 2021-05-24 11:29:36 +0530 | [diff] [blame] | 150 | def fetch_pdrs_from_bmc(client): | 
 | 151 |  | 
 | 152 |     """ This is the core function that would use the existing ssh connection | 
 | 153 |         object to connect to BMC and fire the getPDR pldmtool command | 
 | 154 |         and it then agreegates the data received from all the calls into | 
 | 155 |         the respective dictionaries based on the PDR Type. | 
 | 156 |  | 
 | 157 |         Parameters: | 
 | 158 |             client: paramiko ssh client object | 
 | 159 |  | 
 | 160 |     """ | 
 | 161 |  | 
 | 162 |     entity_association_pdr = {} | 
 | 163 |     state_sensor_pdr = {} | 
 | 164 |     state_effecter_pdr = {} | 
 | 165 |     state_effecter_pdr = {} | 
 | 166 |     numeric_pdr = {} | 
 | 167 |     fru_record_set_pdr = {} | 
 | 168 |     tl_pdr = {} | 
| Brad Bishop | 9152313 | 2021-10-14 19:25:37 -0400 | [diff] [blame] | 169 |     for handle_number, my_dic in get_pdrs(client): | 
| Manojkiran Eda | b8cc325 | 2021-05-24 11:29:36 +0530 | [diff] [blame] | 170 |         sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number)) | 
 | 171 |         sys.stdout.flush() | 
 | 172 |         if my_dic["PDRType"] == "Entity Association PDR": | 
 | 173 |             entity_association_pdr[handle_number] = my_dic | 
 | 174 |         if my_dic["PDRType"] == "State Sensor PDR": | 
 | 175 |             state_sensor_pdr[handle_number] = my_dic | 
 | 176 |         if my_dic["PDRType"] == "State Effecter PDR": | 
 | 177 |             state_effecter_pdr[handle_number] = my_dic | 
 | 178 |         if my_dic["PDRType"] == "FRU Record Set PDR": | 
 | 179 |             fru_record_set_pdr[handle_number] = my_dic | 
 | 180 |         if my_dic["PDRType"] == "Terminus Locator PDR": | 
 | 181 |             tl_pdr[handle_number] = my_dic | 
 | 182 |         if my_dic["PDRType"] == "Numeric Effecter PDR": | 
 | 183 |             numeric_pdr[handle_number] = my_dic | 
| Manojkiran Eda | b8cc325 | 2021-05-24 11:29:36 +0530 | [diff] [blame] | 184 |     client.close() | 
 | 185 |  | 
 | 186 |     total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \ | 
 | 187 |         len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \ | 
 | 188 |         len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys()) | 
 | 189 |     print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s") | 
 | 190 |     print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys())) | 
 | 191 |     print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys())) | 
 | 192 |     print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys())) | 
 | 193 |     print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys())) | 
 | 194 |     print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys())) | 
 | 195 |     print("Number of Entity Association PDR's : ", | 
 | 196 |           len(entity_association_pdr.keys())) | 
 | 197 |     return (entity_association_pdr, state_sensor_pdr, | 
 | 198 |             state_effecter_pdr, len(fru_record_set_pdr.keys())) | 
 | 199 |  | 
 | 200 |  | 
 | 201 | def main(): | 
 | 202 |  | 
 | 203 |     """ Create a summary table capturing the information of all the PDR's | 
 | 204 |         from the BMC & also create a diagram that captures the entity | 
 | 205 |         association hierarchy.""" | 
 | 206 |  | 
 | 207 |     parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py') | 
 | 208 |     parser.add_argument('--bmc', type=str, required=True, | 
 | 209 |                         help="BMC IPAddress/BMC Hostname") | 
| Brad Bishop | e0c55c8 | 2021-10-12 17:14:07 -0400 | [diff] [blame] | 210 |     parser.add_argument('--user', type=str, help="BMC username") | 
| Manojkiran Eda | b8cc325 | 2021-05-24 11:29:36 +0530 | [diff] [blame] | 211 |     parser.add_argument('--password', type=str, required=True, | 
 | 212 |                         help="BMC Password") | 
 | 213 |     parser.add_argument('--port', type=int, help="BMC SSH port", | 
 | 214 |                         default=22) | 
 | 215 |     args = parser.parse_args() | 
| Brad Bishop | 70ac60b | 2021-10-04 18:48:43 -0400 | [diff] [blame] | 216 |     client = connect_to_bmc(args.bmc, args.user, args.password, args.port) | 
 | 217 |     association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \ | 
 | 218 |         fetch_pdrs_from_bmc(client) | 
 | 219 |     draw_entity_associations(association_pdr, counter) | 
 | 220 |     prepare_summary_report(state_sensor_pdr, state_effecter_pdr) | 
| Manojkiran Eda | b8cc325 | 2021-05-24 11:29:36 +0530 | [diff] [blame] | 221 |  | 
 | 222 |  | 
 | 223 | if __name__ == "__main__": | 
 | 224 |     main() |