blob: 95a72aaf0f7fc4369639b68ec7a55d9692809b4a [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)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400139 self.status = status
140
141 def get_status(self):
142 return self.status
Brad Bishop260f75a2021-10-15 12:10:29 -0400143
144
145def process_pldmtool_output(stdout_channel, stderr_channel):
146 """ Ensure pldmtool runs without error and if it does fail, detect that and
147 show the pldmtool exit status and it's stderr.
148
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400149 A simpler implementation would just wait for the pldmtool exit status
150 prior to attempting to decode it's stdout. Instead, optimize for the
151 no error case and allow the json decoder to consume pldmtool stdout as
152 soon as it is available (in parallel). This results in the following
153 error scenarios:
154 - pldmtool fails and the decoder fails
155 Ignore the decoder fail and throw PLDMToolError.
156 - pldmtool fails and the decoder doesn't fail
157 Throw PLDMToolError.
158 - pldmtool doesn't fail and the decoder does fail
159 This is a pldmtool bug - re-throw the decoder error.
160
Brad Bishop260f75a2021-10-15 12:10:29 -0400161 Parameters:
162 stdout_channel: file-like stdout channel
163 stderr_channel: file-like stderr channel
164
165 """
166
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400167 status = 0
168 try:
169 data = json.load(stdout_channel)
170 # it's unlikely, but possible, that pldmtool failed but still wrote a
171 # valid json document - so check for that.
172 status = stderr_channel.channel.recv_exit_status()
173 if status == 0:
174 return data
175 except json.decoder.JSONDecodeError:
176 # pldmtool wrote an invalid json document. Check to see if it had
177 # non-zero exit status.
178 status = stderr_channel.channel.recv_exit_status()
179 if status == 0:
180 # pldmtool didn't have non zero exit status, so it wrote an invalid
181 # json document and the JSONDecodeError is the correct error.
182 raise
Brad Bishop260f75a2021-10-15 12:10:29 -0400183
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400184 # pldmtool had a non-zero exit status, so throw an error for that, possibly
185 # discarding a spurious JSONDecodeError exception.
Brad Bishop260f75a2021-10-15 12:10:29 -0400186 raise PLDMToolError(status, "".join(stderr_channel))
187
188
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400189def get_pdrs_one_at_a_time(client):
Brad Bishop91523132021-10-14 19:25:37 -0400190 """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each
191 record in the PDR repository.
192
193 Parameters:
194 client: paramiko ssh client object
195
196 """
197
198 command_fmt = 'pldmtool platform getpdr -d {}'
199 record_handle = 0
200 while True:
201 output = client.exec_command(command_fmt.format(str(record_handle)))
202 _, stdout, stderr = output
Brad Bishop260f75a2021-10-15 12:10:29 -0400203 pdr = process_pldmtool_output(stdout, stderr)
Brad Bishop91523132021-10-14 19:25:37 -0400204 yield record_handle, pdr
205 record_handle = pdr["nextRecordHandle"]
206 if record_handle == 0:
207 break
208
209
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400210def get_all_pdrs_at_once(client):
211 """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each
212 record in the PDR repository. Use pldmtool platform getpdr --all.
213
214 Parameters:
215 client: paramiko ssh client object
216
217 """
218
219 _, stdout, stderr = client.exec_command('pldmtool platform getpdr -a')
220 all_pdrs = process_pldmtool_output(stdout, stderr)
221
222 # Explicitly request record 0 to find out what the real first record is.
223 _, stdout, stderr = client.exec_command('pldmtool platform getpdr -d 0')
224 pdr_0 = process_pldmtool_output(stdout, stderr)
225 record_handle = pdr_0["recordHandle"]
226
227 while True:
228 for pdr in all_pdrs:
229 if pdr["recordHandle"] == record_handle:
230 yield record_handle, pdr
231 record_handle = pdr["nextRecordHandle"]
232 if record_handle == 0:
233 return
234 raise RuntimeError(
235 "Dangling reference to record {}".format(record_handle))
236
237
238def get_pdrs(client):
239 """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each
240 record in the PDR repository. Use pldmtool platform getpdr --all or
241 fallback on getting them one at a time if pldmtool doesn't support the
242 --all option.
243
244 Parameters:
245 client: paramiko ssh client object
246
247 """
248 try:
249 for record_handle, pdr in get_all_pdrs_at_once(client):
250 yield record_handle, pdr
251 return
252 except PLDMToolError as e:
253 # No support for the -a option
254 if e.get_status() != 106:
255 raise
256 except json.decoder.JSONDecodeError as e:
257 # Some versions of pldmtool don't print valid json documents with -a
258 if e.msg != "Extra data":
259 raise
260
261 for record_handle, pdr in get_pdrs_one_at_a_time(client):
262 yield record_handle, pdr
263
264
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530265def fetch_pdrs_from_bmc(client):
266
267 """ This is the core function that would use the existing ssh connection
268 object to connect to BMC and fire the getPDR pldmtool command
269 and it then agreegates the data received from all the calls into
270 the respective dictionaries based on the PDR Type.
271
272 Parameters:
273 client: paramiko ssh client object
274
275 """
276
277 entity_association_pdr = {}
278 state_sensor_pdr = {}
279 state_effecter_pdr = {}
280 state_effecter_pdr = {}
281 numeric_pdr = {}
282 fru_record_set_pdr = {}
283 tl_pdr = {}
Brad Bishop91523132021-10-14 19:25:37 -0400284 for handle_number, my_dic in get_pdrs(client):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530285 sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
286 sys.stdout.flush()
287 if my_dic["PDRType"] == "Entity Association PDR":
288 entity_association_pdr[handle_number] = my_dic
289 if my_dic["PDRType"] == "State Sensor PDR":
290 state_sensor_pdr[handle_number] = my_dic
291 if my_dic["PDRType"] == "State Effecter PDR":
292 state_effecter_pdr[handle_number] = my_dic
293 if my_dic["PDRType"] == "FRU Record Set PDR":
294 fru_record_set_pdr[handle_number] = my_dic
295 if my_dic["PDRType"] == "Terminus Locator PDR":
296 tl_pdr[handle_number] = my_dic
297 if my_dic["PDRType"] == "Numeric Effecter PDR":
298 numeric_pdr[handle_number] = my_dic
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530299 client.close()
300
301 total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
302 len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
303 len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
304 print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
305 print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
306 print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
307 print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
308 print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
309 print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
310 print("Number of Entity Association PDR's : ",
311 len(entity_association_pdr.keys()))
312 return (entity_association_pdr, state_sensor_pdr,
313 state_effecter_pdr, len(fru_record_set_pdr.keys()))
314
315
316def main():
317
318 """ Create a summary table capturing the information of all the PDR's
319 from the BMC & also create a diagram that captures the entity
320 association hierarchy."""
321
322 parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
323 parser.add_argument('--bmc', type=str, required=True,
324 help="BMC IPAddress/BMC Hostname")
Brad Bishope0c55c82021-10-12 17:14:07 -0400325 parser.add_argument('--user', type=str, help="BMC username")
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530326 parser.add_argument('--password', type=str, required=True,
327 help="BMC Password")
328 parser.add_argument('--port', type=int, help="BMC SSH port",
329 default=22)
330 args = parser.parse_args()
Brad Bishop70ac60b2021-10-04 18:48:43 -0400331 client = connect_to_bmc(args.bmc, args.user, args.password, args.port)
332 association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
333 fetch_pdrs_from_bmc(client)
334 draw_entity_associations(association_pdr, counter)
335 prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530336
337
338if __name__ == "__main__":
339 main()