blob: a7baa03278f4e890fad553f1ba5550048717601c [file] [log] [blame]
Manojkiran Edab8cc3252021-05-24 11:29:36 +05301#!/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
Brad Bishop260f75a2021-10-15 12:10:29 -0400129class 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
Brad Bishop91523132021-10-14 19:25:37 -0400158def 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
Brad Bishop260f75a2021-10-15 12:10:29 -0400172 pdr = process_pldmtool_output(stdout, stderr)
Brad Bishop91523132021-10-14 19:25:37 -0400173 yield record_handle, pdr
174 record_handle = pdr["nextRecordHandle"]
175 if record_handle == 0:
176 break
177
178
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530179def 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 = {}
Brad Bishop91523132021-10-14 19:25:37 -0400198 for handle_number, my_dic in get_pdrs(client):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530199 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
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530213 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")
Brad Bishope0c55c82021-10-12 17:14:07 -0400239 parser.add_argument('--user', type=str, help="BMC username")
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530240 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()
Brad Bishop70ac60b2021-10-04 18:48:43 -0400245 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)
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530250
251
252if __name__ == "__main__":
253 main()