blob: d337ef08c83d34e8cd0528ba4eebbe1ddbd6dd98 [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
Brad Bishop9a8192d2021-10-04 19:58:11 -040013import os
Manojkiran Edab8cc3252021-05-24 11:29:36 +053014
15
16def connect_to_bmc(hostname, uname, passwd, port):
17
18 """ This function is responsible to connect to the BMC via
19 ssh and returns a client object.
20
21 Parameters:
22 hostname: hostname/IP address of BMC
23 uname: ssh username of BMC
24 passwd: ssh password of BMC
25 port: ssh port of BMC
26
27 """
28
29 client = paramiko.SSHClient()
30 client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
31 client.connect(hostname, username=uname, password=passwd, port=port)
32 return client
33
34
35def prepare_summary_report(state_sensor_pdr, state_effecter_pdr):
36
37 """ This function is responsible to parse the state sensor pdr
38 and the state effecter pdr dictionaries and creating the
39 summary table.
40
41 Parameters:
42 state_sensor_pdr: list of state sensor pdrs
43 state_effecter_pdr: list of state effecter pdrs
44
45 """
46
47 summary_table = []
48 headers = ["sensor_id", "entity_type", "state_set", "states"]
49 summary_table.append(headers)
50 for value in state_sensor_pdr.values():
51 summary_record = []
52 sensor_possible_states = ''
53 for sensor_state in value["possibleStates[0]"]:
54 sensor_possible_states += sensor_state+"\n"
55 summary_record.extend([value["sensorID"], value["entityType"],
56 value["stateSetID[0]"],
57 sensor_possible_states])
58 summary_table.append(summary_record)
59 print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
60 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
61
62 summary_table = []
63 headers = ["effecter_id", "entity_type", "state_set", "states"]
64 summary_table.append(headers)
65 for value in state_effecter_pdr.values():
66 summary_record = []
67 effecter_possible_states = ''
68 for state in value["possibleStates[0]"]:
69 effecter_possible_states += state+"\n"
70 summary_record.extend([value["effecterID"], value["entityType"],
71 value["stateSetID[0]"],
72 effecter_possible_states])
73 summary_table.append(summary_record)
74 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
75
76
77def draw_entity_associations(pdr, counter):
78
79 """ This function is responsible to create a picture that captures
80 the entity association hierarchy based on the entity association
81 PDR's received from the BMC.
82
83 Parameters:
84 pdr: list of entity association PDR's
85 counter: variable to capture the count of PDR's to unflatten
86 the tree
87
88 """
89
90 dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1',
91 'style': 'filled'})
92 dot.attr(label=r'\n\nEntity Relation Diagram < ' +
93 str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n')
94 dot.attr(fontsize='20')
95 edge_list = []
96 for value in pdr.values():
97 parentnode = str(value["containerEntityType"]) + \
98 str(value["containerEntityInstanceNumber"])
99 dot.node(hashlib.md5((parentnode +
100 str(value["containerEntityContainerID"]))
101 .encode()).hexdigest(), parentnode)
102
103 for i in range(1, value["containedEntityCount"]+1):
104 childnode = str(value[f"containedEntityType[{i}]"]) + \
105 str(value[f"containedEntityInstanceNumber[{i}]"])
106 cid = str(value[f"containedEntityContainerID[{i}]"])
107 dot.node(hashlib.md5((childnode + cid)
108 .encode()).hexdigest(), childnode)
109
110 if[hashlib.md5((parentnode +
111 str(value["containerEntityContainerID"]))
112 .encode()).hexdigest(),
113 hashlib.md5((childnode + cid)
114 .encode()).hexdigest()] not in edge_list:
115 edge_list.append([hashlib.md5((parentnode +
116 str(value["containerEntityContainerID"]))
117 .encode()).hexdigest(),
118 hashlib.md5((childnode + cid)
119 .encode()).hexdigest()])
120 dot.edge(hashlib.md5((parentnode +
121 str(value["containerEntityContainerID"]))
122 .encode()).hexdigest(),
123 hashlib.md5((childnode + cid).encode()).hexdigest())
124 unflattentree = dot.unflatten(stagger=(round(counter/3)))
125 unflattentree.render(filename='entity_association_' +
126 str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")),
127 view=False, cleanup=True, format='pdf')
128
129
Brad Bishop260f75a2021-10-15 12:10:29 -0400130class PLDMToolError(Exception):
131 """ Exception class intended to be used to hold pldmtool invocation failure
132 information such as exit status and stderr.
133
134 """
135
136 def __init__(self, status, stderr):
137 msg = "pldmtool failed with exit status {}.\n".format(status)
138 msg += "stderr: \n\n{}".format(stderr)
139 super(PLDMToolError, self).__init__(msg)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400140 self.status = status
141
142 def get_status(self):
143 return self.status
Brad Bishop260f75a2021-10-15 12:10:29 -0400144
145
146def process_pldmtool_output(stdout_channel, stderr_channel):
147 """ Ensure pldmtool runs without error and if it does fail, detect that and
148 show the pldmtool exit status and it's stderr.
149
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400150 A simpler implementation would just wait for the pldmtool exit status
151 prior to attempting to decode it's stdout. Instead, optimize for the
152 no error case and allow the json decoder to consume pldmtool stdout as
153 soon as it is available (in parallel). This results in the following
154 error scenarios:
155 - pldmtool fails and the decoder fails
156 Ignore the decoder fail and throw PLDMToolError.
157 - pldmtool fails and the decoder doesn't fail
158 Throw PLDMToolError.
159 - pldmtool doesn't fail and the decoder does fail
160 This is a pldmtool bug - re-throw the decoder error.
161
Brad Bishop260f75a2021-10-15 12:10:29 -0400162 Parameters:
163 stdout_channel: file-like stdout channel
164 stderr_channel: file-like stderr channel
165
166 """
167
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400168 status = 0
169 try:
170 data = json.load(stdout_channel)
171 # it's unlikely, but possible, that pldmtool failed but still wrote a
172 # valid json document - so check for that.
173 status = stderr_channel.channel.recv_exit_status()
174 if status == 0:
175 return data
176 except json.decoder.JSONDecodeError:
177 # pldmtool wrote an invalid json document. Check to see if it had
178 # non-zero exit status.
179 status = stderr_channel.channel.recv_exit_status()
180 if status == 0:
181 # pldmtool didn't have non zero exit status, so it wrote an invalid
182 # json document and the JSONDecodeError is the correct error.
183 raise
Brad Bishop260f75a2021-10-15 12:10:29 -0400184
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400185 # pldmtool had a non-zero exit status, so throw an error for that, possibly
186 # discarding a spurious JSONDecodeError exception.
Brad Bishop260f75a2021-10-15 12:10:29 -0400187 raise PLDMToolError(status, "".join(stderr_channel))
188
189
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400190def get_pdrs_one_at_a_time(client):
Brad Bishop91523132021-10-14 19:25:37 -0400191 """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each
192 record in the PDR repository.
193
194 Parameters:
195 client: paramiko ssh client object
196
197 """
198
199 command_fmt = 'pldmtool platform getpdr -d {}'
200 record_handle = 0
201 while True:
202 output = client.exec_command(command_fmt.format(str(record_handle)))
203 _, stdout, stderr = output
Brad Bishop260f75a2021-10-15 12:10:29 -0400204 pdr = process_pldmtool_output(stdout, stderr)
Brad Bishop91523132021-10-14 19:25:37 -0400205 yield record_handle, pdr
206 record_handle = pdr["nextRecordHandle"]
207 if record_handle == 0:
208 break
209
210
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400211def get_all_pdrs_at_once(client):
212 """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each
213 record in the PDR repository. Use pldmtool platform getpdr --all.
214
215 Parameters:
216 client: paramiko ssh client object
217
218 """
219
220 _, stdout, stderr = client.exec_command('pldmtool platform getpdr -a')
221 all_pdrs = process_pldmtool_output(stdout, stderr)
222
223 # Explicitly request record 0 to find out what the real first record is.
224 _, stdout, stderr = client.exec_command('pldmtool platform getpdr -d 0')
225 pdr_0 = process_pldmtool_output(stdout, stderr)
226 record_handle = pdr_0["recordHandle"]
227
228 while True:
229 for pdr in all_pdrs:
230 if pdr["recordHandle"] == record_handle:
231 yield record_handle, pdr
232 record_handle = pdr["nextRecordHandle"]
233 if record_handle == 0:
234 return
235 raise RuntimeError(
236 "Dangling reference to record {}".format(record_handle))
237
238
239def get_pdrs(client):
240 """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each
241 record in the PDR repository. Use pldmtool platform getpdr --all or
242 fallback on getting them one at a time if pldmtool doesn't support the
243 --all option.
244
245 Parameters:
246 client: paramiko ssh client object
247
248 """
249 try:
250 for record_handle, pdr in get_all_pdrs_at_once(client):
251 yield record_handle, pdr
252 return
253 except PLDMToolError as e:
254 # No support for the -a option
255 if e.get_status() != 106:
256 raise
257 except json.decoder.JSONDecodeError as e:
258 # Some versions of pldmtool don't print valid json documents with -a
259 if e.msg != "Extra data":
260 raise
261
262 for record_handle, pdr in get_pdrs_one_at_a_time(client):
263 yield record_handle, pdr
264
265
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530266def fetch_pdrs_from_bmc(client):
267
268 """ This is the core function that would use the existing ssh connection
269 object to connect to BMC and fire the getPDR pldmtool command
270 and it then agreegates the data received from all the calls into
271 the respective dictionaries based on the PDR Type.
272
273 Parameters:
274 client: paramiko ssh client object
275
276 """
277
278 entity_association_pdr = {}
279 state_sensor_pdr = {}
280 state_effecter_pdr = {}
281 state_effecter_pdr = {}
282 numeric_pdr = {}
283 fru_record_set_pdr = {}
284 tl_pdr = {}
Brad Bishop91523132021-10-14 19:25:37 -0400285 for handle_number, my_dic in get_pdrs(client):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530286 sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
287 sys.stdout.flush()
288 if my_dic["PDRType"] == "Entity Association PDR":
289 entity_association_pdr[handle_number] = my_dic
290 if my_dic["PDRType"] == "State Sensor PDR":
291 state_sensor_pdr[handle_number] = my_dic
292 if my_dic["PDRType"] == "State Effecter PDR":
293 state_effecter_pdr[handle_number] = my_dic
294 if my_dic["PDRType"] == "FRU Record Set PDR":
295 fru_record_set_pdr[handle_number] = my_dic
296 if my_dic["PDRType"] == "Terminus Locator PDR":
297 tl_pdr[handle_number] = my_dic
298 if my_dic["PDRType"] == "Numeric Effecter PDR":
299 numeric_pdr[handle_number] = my_dic
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530300 client.close()
301
302 total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
303 len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
304 len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
305 print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
306 print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
307 print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
308 print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
309 print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
310 print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
311 print("Number of Entity Association PDR's : ",
312 len(entity_association_pdr.keys()))
313 return (entity_association_pdr, state_sensor_pdr,
314 state_effecter_pdr, len(fru_record_set_pdr.keys()))
315
316
317def main():
318
319 """ Create a summary table capturing the information of all the PDR's
320 from the BMC & also create a diagram that captures the entity
321 association hierarchy."""
322
323 parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
324 parser.add_argument('--bmc', type=str, required=True,
325 help="BMC IPAddress/BMC Hostname")
Brad Bishope0c55c82021-10-12 17:14:07 -0400326 parser.add_argument('--user', type=str, help="BMC username")
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530327 parser.add_argument('--password', type=str, required=True,
328 help="BMC Password")
329 parser.add_argument('--port', type=int, help="BMC SSH port",
330 default=22)
331 args = parser.parse_args()
Brad Bishop9a8192d2021-10-04 19:58:11 -0400332
333 try:
334 with open(os.path.expanduser("~/.ssh/config")) as f:
335 ssh_config = paramiko.SSHConfig()
336 ssh_config.parse(f)
337 host_config = ssh_config.lookup(args.bmc)
338 if host_config:
339 if 'hostname' in host_config:
340 args.bmc = host_config['hostname']
341 if 'user' in host_config and args.user is None:
342 args.user = host_config['user']
343 except FileNotFoundError:
344 pass
345
Brad Bishop70ac60b2021-10-04 18:48:43 -0400346 client = connect_to_bmc(args.bmc, args.user, args.password, args.port)
347 association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
348 fetch_pdrs_from_bmc(client)
349 draw_entity_associations(association_pdr, counter)
350 prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530351
352
353if __name__ == "__main__":
354 main()