blob: 32f0222bf0e8f923ca10ce64bf093d78073da0f5 [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
Brad Bishop7dc416f2021-10-20 11:45:08 -040016class Executor:
17 """ Interface definition for interacting with executors. An executor is an
18 object that can run a program."""
Manojkiran Edab8cc3252021-05-24 11:29:36 +053019
Brad Bishop7dc416f2021-10-20 11:45:08 -040020 def exec_command(self, cmd):
21 raise NotImplementedError
Manojkiran Edab8cc3252021-05-24 11:29:36 +053022
Brad Bishop7dc416f2021-10-20 11:45:08 -040023 def close(self):
24 pass
Manojkiran Edab8cc3252021-05-24 11:29:36 +053025
Manojkiran Edab8cc3252021-05-24 11:29:36 +053026
Brad Bishop7dc416f2021-10-20 11:45:08 -040027class ParamikoExecutor(Executor):
28 """ Concrete implementation of the Executor interface that uses
29 Paramiko to connect to a remote BMC to run the program."""
30
31 def __init__(self, hostname, uname, passwd, port, **kw):
32 """ This function is responsible for connecting to the BMC via
33 ssh and returning an executor object.
34
35 Parameters:
36 hostname: hostname/IP address of BMC
37 uname: ssh username of BMC
38 passwd: ssh password of BMC
39 port: ssh port of BMC
40 """
41
42 super(ParamikoExecutor, self).__init__()
43 self.client = paramiko.SSHClient()
44 self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
45 self.client.connect(
46 hostname, username=uname, password=passwd, port=port, **kw)
47
48 def exec_command(self, cmd):
49 return self.client.exec_command(cmd)
50
51 def close(self):
52 self.client.close()
Manojkiran Edab8cc3252021-05-24 11:29:36 +053053
54
55def prepare_summary_report(state_sensor_pdr, state_effecter_pdr):
56
57 """ This function is responsible to parse the state sensor pdr
58 and the state effecter pdr dictionaries and creating the
59 summary table.
60
61 Parameters:
62 state_sensor_pdr: list of state sensor pdrs
63 state_effecter_pdr: list of state effecter pdrs
64
65 """
66
67 summary_table = []
68 headers = ["sensor_id", "entity_type", "state_set", "states"]
69 summary_table.append(headers)
70 for value in state_sensor_pdr.values():
71 summary_record = []
72 sensor_possible_states = ''
73 for sensor_state in value["possibleStates[0]"]:
74 sensor_possible_states += sensor_state+"\n"
75 summary_record.extend([value["sensorID"], value["entityType"],
76 value["stateSetID[0]"],
77 sensor_possible_states])
78 summary_table.append(summary_record)
79 print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
80 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
81
82 summary_table = []
83 headers = ["effecter_id", "entity_type", "state_set", "states"]
84 summary_table.append(headers)
85 for value in state_effecter_pdr.values():
86 summary_record = []
87 effecter_possible_states = ''
88 for state in value["possibleStates[0]"]:
89 effecter_possible_states += state+"\n"
90 summary_record.extend([value["effecterID"], value["entityType"],
91 value["stateSetID[0]"],
92 effecter_possible_states])
93 summary_table.append(summary_record)
94 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
95
96
97def draw_entity_associations(pdr, counter):
98
99 """ This function is responsible to create a picture that captures
100 the entity association hierarchy based on the entity association
101 PDR's received from the BMC.
102
103 Parameters:
104 pdr: list of entity association PDR's
105 counter: variable to capture the count of PDR's to unflatten
106 the tree
107
108 """
109
110 dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1',
111 'style': 'filled'})
112 dot.attr(label=r'\n\nEntity Relation Diagram < ' +
113 str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n')
114 dot.attr(fontsize='20')
115 edge_list = []
116 for value in pdr.values():
117 parentnode = str(value["containerEntityType"]) + \
118 str(value["containerEntityInstanceNumber"])
119 dot.node(hashlib.md5((parentnode +
120 str(value["containerEntityContainerID"]))
121 .encode()).hexdigest(), parentnode)
122
123 for i in range(1, value["containedEntityCount"]+1):
124 childnode = str(value[f"containedEntityType[{i}]"]) + \
125 str(value[f"containedEntityInstanceNumber[{i}]"])
126 cid = str(value[f"containedEntityContainerID[{i}]"])
127 dot.node(hashlib.md5((childnode + cid)
128 .encode()).hexdigest(), childnode)
129
130 if[hashlib.md5((parentnode +
131 str(value["containerEntityContainerID"]))
132 .encode()).hexdigest(),
133 hashlib.md5((childnode + cid)
134 .encode()).hexdigest()] not in edge_list:
135 edge_list.append([hashlib.md5((parentnode +
136 str(value["containerEntityContainerID"]))
137 .encode()).hexdigest(),
138 hashlib.md5((childnode + cid)
139 .encode()).hexdigest()])
140 dot.edge(hashlib.md5((parentnode +
141 str(value["containerEntityContainerID"]))
142 .encode()).hexdigest(),
143 hashlib.md5((childnode + cid).encode()).hexdigest())
144 unflattentree = dot.unflatten(stagger=(round(counter/3)))
145 unflattentree.render(filename='entity_association_' +
146 str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")),
147 view=False, cleanup=True, format='pdf')
148
149
Brad Bishop260f75a2021-10-15 12:10:29 -0400150class PLDMToolError(Exception):
151 """ Exception class intended to be used to hold pldmtool invocation failure
152 information such as exit status and stderr.
153
154 """
155
156 def __init__(self, status, stderr):
157 msg = "pldmtool failed with exit status {}.\n".format(status)
158 msg += "stderr: \n\n{}".format(stderr)
159 super(PLDMToolError, self).__init__(msg)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400160 self.status = status
161
162 def get_status(self):
163 return self.status
Brad Bishop260f75a2021-10-15 12:10:29 -0400164
165
166def process_pldmtool_output(stdout_channel, stderr_channel):
167 """ Ensure pldmtool runs without error and if it does fail, detect that and
168 show the pldmtool exit status and it's stderr.
169
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400170 A simpler implementation would just wait for the pldmtool exit status
171 prior to attempting to decode it's stdout. Instead, optimize for the
172 no error case and allow the json decoder to consume pldmtool stdout as
173 soon as it is available (in parallel). This results in the following
174 error scenarios:
175 - pldmtool fails and the decoder fails
176 Ignore the decoder fail and throw PLDMToolError.
177 - pldmtool fails and the decoder doesn't fail
178 Throw PLDMToolError.
179 - pldmtool doesn't fail and the decoder does fail
180 This is a pldmtool bug - re-throw the decoder error.
181
Brad Bishop260f75a2021-10-15 12:10:29 -0400182 Parameters:
183 stdout_channel: file-like stdout channel
184 stderr_channel: file-like stderr channel
185
186 """
187
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400188 status = 0
189 try:
190 data = json.load(stdout_channel)
191 # it's unlikely, but possible, that pldmtool failed but still wrote a
192 # valid json document - so check for that.
193 status = stderr_channel.channel.recv_exit_status()
194 if status == 0:
195 return data
196 except json.decoder.JSONDecodeError:
197 # pldmtool wrote an invalid json document. Check to see if it had
198 # non-zero exit status.
199 status = stderr_channel.channel.recv_exit_status()
200 if status == 0:
201 # pldmtool didn't have non zero exit status, so it wrote an invalid
202 # json document and the JSONDecodeError is the correct error.
203 raise
Brad Bishop260f75a2021-10-15 12:10:29 -0400204
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400205 # pldmtool had a non-zero exit status, so throw an error for that, possibly
206 # discarding a spurious JSONDecodeError exception.
Brad Bishop260f75a2021-10-15 12:10:29 -0400207 raise PLDMToolError(status, "".join(stderr_channel))
208
209
Brad Bishop7dc416f2021-10-20 11:45:08 -0400210def get_pdrs_one_at_a_time(executor):
211 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
212 the PDR repository.
Brad Bishop91523132021-10-14 19:25:37 -0400213
214 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400215 executor: executor object for running pldmtool
Brad Bishop91523132021-10-14 19:25:37 -0400216
217 """
218
219 command_fmt = 'pldmtool platform getpdr -d {}'
220 record_handle = 0
221 while True:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400222 output = executor.exec_command(command_fmt.format(str(record_handle)))
Brad Bishop91523132021-10-14 19:25:37 -0400223 _, stdout, stderr = output
Brad Bishop260f75a2021-10-15 12:10:29 -0400224 pdr = process_pldmtool_output(stdout, stderr)
Brad Bishop91523132021-10-14 19:25:37 -0400225 yield record_handle, pdr
226 record_handle = pdr["nextRecordHandle"]
227 if record_handle == 0:
228 break
229
230
Brad Bishop7dc416f2021-10-20 11:45:08 -0400231def get_all_pdrs_at_once(executor):
232 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
233 the PDR repository. Use pldmtool platform getpdr --all.
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400234
235 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400236 executor: executor object for running pldmtool
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400237
238 """
239
Brad Bishop7dc416f2021-10-20 11:45:08 -0400240 _, stdout, stderr = executor.exec_command('pldmtool platform getpdr -a')
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400241 all_pdrs = process_pldmtool_output(stdout, stderr)
242
243 # Explicitly request record 0 to find out what the real first record is.
Brad Bishop7dc416f2021-10-20 11:45:08 -0400244 _, stdout, stderr = executor.exec_command('pldmtool platform getpdr -d 0')
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400245 pdr_0 = process_pldmtool_output(stdout, stderr)
246 record_handle = pdr_0["recordHandle"]
247
248 while True:
249 for pdr in all_pdrs:
250 if pdr["recordHandle"] == record_handle:
251 yield record_handle, pdr
252 record_handle = pdr["nextRecordHandle"]
253 if record_handle == 0:
254 return
255 raise RuntimeError(
256 "Dangling reference to record {}".format(record_handle))
257
258
Brad Bishop7dc416f2021-10-20 11:45:08 -0400259def get_pdrs(executor):
260 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
261 the PDR repository. Use pldmtool platform getpdr --all or fallback on
262 getting them one at a time if pldmtool doesn't support the --all
263 option.
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400264
265 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400266 executor: executor object for running pldmtool
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400267
268 """
269 try:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400270 for record_handle, pdr in get_all_pdrs_at_once(executor):
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400271 yield record_handle, pdr
272 return
273 except PLDMToolError as e:
274 # No support for the -a option
275 if e.get_status() != 106:
276 raise
277 except json.decoder.JSONDecodeError as e:
278 # Some versions of pldmtool don't print valid json documents with -a
279 if e.msg != "Extra data":
280 raise
281
Brad Bishop7dc416f2021-10-20 11:45:08 -0400282 for record_handle, pdr in get_pdrs_one_at_a_time(executor):
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400283 yield record_handle, pdr
284
285
Brad Bishop7dc416f2021-10-20 11:45:08 -0400286def fetch_pdrs_from_bmc(executor):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530287
Brad Bishop7dc416f2021-10-20 11:45:08 -0400288 """ This is the core function that would fire the getPDR pldmtool command
289 and it then agreegates the data received from all the calls into the
290 respective dictionaries based on the PDR Type.
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530291
292 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400293 executor: executor object for running pldmtool
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530294
295 """
296
297 entity_association_pdr = {}
298 state_sensor_pdr = {}
299 state_effecter_pdr = {}
300 state_effecter_pdr = {}
301 numeric_pdr = {}
302 fru_record_set_pdr = {}
303 tl_pdr = {}
Brad Bishop7dc416f2021-10-20 11:45:08 -0400304 for handle_number, my_dic in get_pdrs(executor):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530305 sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
306 sys.stdout.flush()
307 if my_dic["PDRType"] == "Entity Association PDR":
308 entity_association_pdr[handle_number] = my_dic
309 if my_dic["PDRType"] == "State Sensor PDR":
310 state_sensor_pdr[handle_number] = my_dic
311 if my_dic["PDRType"] == "State Effecter PDR":
312 state_effecter_pdr[handle_number] = my_dic
313 if my_dic["PDRType"] == "FRU Record Set PDR":
314 fru_record_set_pdr[handle_number] = my_dic
315 if my_dic["PDRType"] == "Terminus Locator PDR":
316 tl_pdr[handle_number] = my_dic
317 if my_dic["PDRType"] == "Numeric Effecter PDR":
318 numeric_pdr[handle_number] = my_dic
Brad Bishop7dc416f2021-10-20 11:45:08 -0400319 executor.close()
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530320
321 total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
322 len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
323 len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
324 print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
325 print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
326 print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
327 print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
328 print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
329 print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
330 print("Number of Entity Association PDR's : ",
331 len(entity_association_pdr.keys()))
332 return (entity_association_pdr, state_sensor_pdr,
333 state_effecter_pdr, len(fru_record_set_pdr.keys()))
334
335
336def main():
337
338 """ Create a summary table capturing the information of all the PDR's
339 from the BMC & also create a diagram that captures the entity
340 association hierarchy."""
341
342 parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
343 parser.add_argument('--bmc', type=str, required=True,
344 help="BMC IPAddress/BMC Hostname")
Brad Bishope0c55c82021-10-12 17:14:07 -0400345 parser.add_argument('--user', type=str, help="BMC username")
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530346 parser.add_argument('--password', type=str, required=True,
347 help="BMC Password")
348 parser.add_argument('--port', type=int, help="BMC SSH port",
349 default=22)
350 args = parser.parse_args()
Brad Bishop9a8192d2021-10-04 19:58:11 -0400351
Brad Bishop9e0265b2021-10-04 20:14:25 -0400352 extra_cfg = {}
Brad Bishop9a8192d2021-10-04 19:58:11 -0400353 try:
354 with open(os.path.expanduser("~/.ssh/config")) as f:
355 ssh_config = paramiko.SSHConfig()
356 ssh_config.parse(f)
357 host_config = ssh_config.lookup(args.bmc)
358 if host_config:
359 if 'hostname' in host_config:
360 args.bmc = host_config['hostname']
361 if 'user' in host_config and args.user is None:
362 args.user = host_config['user']
Brad Bishop9e0265b2021-10-04 20:14:25 -0400363 if 'proxycommand' in host_config:
364 extra_cfg['sock'] = paramiko.ProxyCommand(
365 host_config['proxycommand'])
Brad Bishop9a8192d2021-10-04 19:58:11 -0400366 except FileNotFoundError:
367 pass
368
Brad Bishop7dc416f2021-10-20 11:45:08 -0400369 executor = ParamikoExecutor(
Brad Bishop9e0265b2021-10-04 20:14:25 -0400370 args.bmc, args.user, args.password, args.port, **extra_cfg)
Brad Bishop70ac60b2021-10-04 18:48:43 -0400371 association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
Brad Bishop7dc416f2021-10-20 11:45:08 -0400372 fetch_pdrs_from_bmc(executor)
Brad Bishop70ac60b2021-10-04 18:48:43 -0400373 draw_entity_associations(association_pdr, counter)
374 prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530375
376
377if __name__ == "__main__":
378 main()