blob: 361f4334a288f727cd4b91ef0f9e107c7e4e7709 [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
Brad Bishopc8906372021-10-19 15:44:38 -040014import shlex
15import shutil
16import subprocess
Manojkiran Edab8cc3252021-05-24 11:29:36 +053017
18
Brad Bishop241e0682021-10-19 15:03:39 -040019class Process:
20 """ Interface definition for interacting with a process created by an
21 Executor."""
22
23 def __init__(self, stdout, stderr):
24 """ Construct a Process object. Process object clients can read the
25 process stdout and stderr with os.read(), and can wait for the
26 process to exit.
27
28 Parameters:
29 stdout: os.read()able stream representing stdout
30 stderr: os.read()able stream representing stderr
31 """
32
33 self.stdout = stdout
34 self.stderr = stderr
35
36 def wait(self):
37 """ Wait for the process to finish, and return its exit status."""
38
39 raise NotImplementedError
40
41
Brad Bishop7dc416f2021-10-20 11:45:08 -040042class Executor:
43 """ Interface definition for interacting with executors. An executor is an
44 object that can run a program."""
Manojkiran Edab8cc3252021-05-24 11:29:36 +053045
Brad Bishop7dc416f2021-10-20 11:45:08 -040046 def exec_command(self, cmd):
47 raise NotImplementedError
Manojkiran Edab8cc3252021-05-24 11:29:36 +053048
Brad Bishop7dc416f2021-10-20 11:45:08 -040049 def close(self):
50 pass
Manojkiran Edab8cc3252021-05-24 11:29:36 +053051
Manojkiran Edab8cc3252021-05-24 11:29:36 +053052
Brad Bishop241e0682021-10-19 15:03:39 -040053class ParamikoProcess(Process):
54 """ Concrete implementation of the Process interface that adapts Paramiko
55 interfaces to the Process interface requirements."""
56
57 def __init__(self, stdout, stderr):
58 super(ParamikoProcess, self).__init__(stdout, stderr)
59
60 def wait(self):
61 return self.stderr.channel.recv_exit_status()
62
63
Brad Bishop7dc416f2021-10-20 11:45:08 -040064class ParamikoExecutor(Executor):
65 """ Concrete implementation of the Executor interface that uses
66 Paramiko to connect to a remote BMC to run the program."""
67
68 def __init__(self, hostname, uname, passwd, port, **kw):
69 """ This function is responsible for connecting to the BMC via
70 ssh and returning an executor object.
71
72 Parameters:
73 hostname: hostname/IP address of BMC
74 uname: ssh username of BMC
75 passwd: ssh password of BMC
76 port: ssh port of BMC
77 """
78
79 super(ParamikoExecutor, self).__init__()
80 self.client = paramiko.SSHClient()
81 self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
82 self.client.connect(
83 hostname, username=uname, password=passwd, port=port, **kw)
84
85 def exec_command(self, cmd):
Brad Bishop241e0682021-10-19 15:03:39 -040086 _, stdout, stderr = self.client.exec_command(cmd)
87 return ParamikoProcess(stdout, stderr)
Brad Bishop7dc416f2021-10-20 11:45:08 -040088
89 def close(self):
90 self.client.close()
Manojkiran Edab8cc3252021-05-24 11:29:36 +053091
92
Brad Bishopc8906372021-10-19 15:44:38 -040093class SubprocessProcess(Process):
94 def __init__(self, popen):
95 self.popen = popen
96 super(SubprocessProcess, self).__init__(popen.stdout, popen.stderr)
97
98 def wait(self):
99 self.popen.wait()
100 return self.popen.returncode
101
102
103class SubprocessExecutor(Executor):
104 def __init__(self):
105 super(SubprocessExecutor, self).__init__()
106
107 def exec_command(self, cmd):
108 args = shlex.split(cmd)
109 args[0] = shutil.which(args[0])
110 p = subprocess.Popen(
111 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
112 return SubprocessProcess(p)
113
114
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530115def prepare_summary_report(state_sensor_pdr, state_effecter_pdr):
116
117 """ This function is responsible to parse the state sensor pdr
118 and the state effecter pdr dictionaries and creating the
119 summary table.
120
121 Parameters:
122 state_sensor_pdr: list of state sensor pdrs
123 state_effecter_pdr: list of state effecter pdrs
124
125 """
126
127 summary_table = []
128 headers = ["sensor_id", "entity_type", "state_set", "states"]
129 summary_table.append(headers)
130 for value in state_sensor_pdr.values():
131 summary_record = []
132 sensor_possible_states = ''
133 for sensor_state in value["possibleStates[0]"]:
134 sensor_possible_states += sensor_state+"\n"
135 summary_record.extend([value["sensorID"], value["entityType"],
136 value["stateSetID[0]"],
137 sensor_possible_states])
138 summary_table.append(summary_record)
139 print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
140 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
141
142 summary_table = []
143 headers = ["effecter_id", "entity_type", "state_set", "states"]
144 summary_table.append(headers)
145 for value in state_effecter_pdr.values():
146 summary_record = []
147 effecter_possible_states = ''
148 for state in value["possibleStates[0]"]:
149 effecter_possible_states += state+"\n"
150 summary_record.extend([value["effecterID"], value["entityType"],
151 value["stateSetID[0]"],
152 effecter_possible_states])
153 summary_table.append(summary_record)
154 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
155
156
157def draw_entity_associations(pdr, counter):
158
159 """ This function is responsible to create a picture that captures
160 the entity association hierarchy based on the entity association
161 PDR's received from the BMC.
162
163 Parameters:
164 pdr: list of entity association PDR's
165 counter: variable to capture the count of PDR's to unflatten
166 the tree
167
168 """
169
170 dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1',
171 'style': 'filled'})
172 dot.attr(label=r'\n\nEntity Relation Diagram < ' +
173 str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n')
174 dot.attr(fontsize='20')
175 edge_list = []
176 for value in pdr.values():
177 parentnode = str(value["containerEntityType"]) + \
178 str(value["containerEntityInstanceNumber"])
179 dot.node(hashlib.md5((parentnode +
180 str(value["containerEntityContainerID"]))
181 .encode()).hexdigest(), parentnode)
182
183 for i in range(1, value["containedEntityCount"]+1):
184 childnode = str(value[f"containedEntityType[{i}]"]) + \
185 str(value[f"containedEntityInstanceNumber[{i}]"])
186 cid = str(value[f"containedEntityContainerID[{i}]"])
187 dot.node(hashlib.md5((childnode + cid)
188 .encode()).hexdigest(), childnode)
189
190 if[hashlib.md5((parentnode +
191 str(value["containerEntityContainerID"]))
192 .encode()).hexdigest(),
193 hashlib.md5((childnode + cid)
194 .encode()).hexdigest()] not in edge_list:
195 edge_list.append([hashlib.md5((parentnode +
196 str(value["containerEntityContainerID"]))
197 .encode()).hexdigest(),
198 hashlib.md5((childnode + cid)
199 .encode()).hexdigest()])
200 dot.edge(hashlib.md5((parentnode +
201 str(value["containerEntityContainerID"]))
202 .encode()).hexdigest(),
203 hashlib.md5((childnode + cid).encode()).hexdigest())
204 unflattentree = dot.unflatten(stagger=(round(counter/3)))
205 unflattentree.render(filename='entity_association_' +
206 str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")),
207 view=False, cleanup=True, format='pdf')
208
209
Brad Bishop260f75a2021-10-15 12:10:29 -0400210class PLDMToolError(Exception):
211 """ Exception class intended to be used to hold pldmtool invocation failure
212 information such as exit status and stderr.
213
214 """
215
216 def __init__(self, status, stderr):
217 msg = "pldmtool failed with exit status {}.\n".format(status)
218 msg += "stderr: \n\n{}".format(stderr)
219 super(PLDMToolError, self).__init__(msg)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400220 self.status = status
221
222 def get_status(self):
223 return self.status
Brad Bishop260f75a2021-10-15 12:10:29 -0400224
225
Brad Bishop241e0682021-10-19 15:03:39 -0400226def process_pldmtool_output(process):
Brad Bishop260f75a2021-10-15 12:10:29 -0400227 """ Ensure pldmtool runs without error and if it does fail, detect that and
228 show the pldmtool exit status and it's stderr.
229
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400230 A simpler implementation would just wait for the pldmtool exit status
231 prior to attempting to decode it's stdout. Instead, optimize for the
232 no error case and allow the json decoder to consume pldmtool stdout as
233 soon as it is available (in parallel). This results in the following
234 error scenarios:
235 - pldmtool fails and the decoder fails
236 Ignore the decoder fail and throw PLDMToolError.
237 - pldmtool fails and the decoder doesn't fail
238 Throw PLDMToolError.
239 - pldmtool doesn't fail and the decoder does fail
240 This is a pldmtool bug - re-throw the decoder error.
241
Brad Bishop260f75a2021-10-15 12:10:29 -0400242 Parameters:
Brad Bishop241e0682021-10-19 15:03:39 -0400243 process: A Process object providing process control functions like
244 wait, and access functions such as reading stdout and
245 stderr.
Brad Bishop260f75a2021-10-15 12:10:29 -0400246
247 """
248
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400249 status = 0
250 try:
Brad Bishop241e0682021-10-19 15:03:39 -0400251 data = json.load(process.stdout)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400252 # it's unlikely, but possible, that pldmtool failed but still wrote a
253 # valid json document - so check for that.
Brad Bishop241e0682021-10-19 15:03:39 -0400254 status = process.wait()
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400255 if status == 0:
256 return data
257 except json.decoder.JSONDecodeError:
258 # pldmtool wrote an invalid json document. Check to see if it had
259 # non-zero exit status.
Brad Bishop241e0682021-10-19 15:03:39 -0400260 status = process.wait()
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400261 if status == 0:
262 # pldmtool didn't have non zero exit status, so it wrote an invalid
263 # json document and the JSONDecodeError is the correct error.
264 raise
Brad Bishop260f75a2021-10-15 12:10:29 -0400265
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400266 # pldmtool had a non-zero exit status, so throw an error for that, possibly
267 # discarding a spurious JSONDecodeError exception.
Brad Bishop241e0682021-10-19 15:03:39 -0400268 raise PLDMToolError(status, "".join(process.stderr))
Brad Bishop260f75a2021-10-15 12:10:29 -0400269
270
Brad Bishop7dc416f2021-10-20 11:45:08 -0400271def get_pdrs_one_at_a_time(executor):
272 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
273 the PDR repository.
Brad Bishop91523132021-10-14 19:25:37 -0400274
275 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400276 executor: executor object for running pldmtool
Brad Bishop91523132021-10-14 19:25:37 -0400277
278 """
279
280 command_fmt = 'pldmtool platform getpdr -d {}'
281 record_handle = 0
282 while True:
Brad Bishop241e0682021-10-19 15:03:39 -0400283 process = executor.exec_command(command_fmt.format(str(record_handle)))
284 pdr = process_pldmtool_output(process)
Brad Bishop91523132021-10-14 19:25:37 -0400285 yield record_handle, pdr
286 record_handle = pdr["nextRecordHandle"]
287 if record_handle == 0:
288 break
289
290
Brad Bishop7dc416f2021-10-20 11:45:08 -0400291def get_all_pdrs_at_once(executor):
292 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
293 the PDR repository. Use pldmtool platform getpdr --all.
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400294
295 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400296 executor: executor object for running pldmtool
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400297
298 """
299
Brad Bishop241e0682021-10-19 15:03:39 -0400300 process = executor.exec_command('pldmtool platform getpdr -a')
301 all_pdrs = process_pldmtool_output(process)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400302
303 # Explicitly request record 0 to find out what the real first record is.
Brad Bishop241e0682021-10-19 15:03:39 -0400304 process = executor.exec_command('pldmtool platform getpdr -d 0')
305 pdr_0 = process_pldmtool_output(process)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400306 record_handle = pdr_0["recordHandle"]
307
308 while True:
309 for pdr in all_pdrs:
310 if pdr["recordHandle"] == record_handle:
311 yield record_handle, pdr
312 record_handle = pdr["nextRecordHandle"]
313 if record_handle == 0:
314 return
315 raise RuntimeError(
316 "Dangling reference to record {}".format(record_handle))
317
318
Brad Bishop7dc416f2021-10-20 11:45:08 -0400319def get_pdrs(executor):
320 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
321 the PDR repository. Use pldmtool platform getpdr --all or fallback on
322 getting them one at a time if pldmtool doesn't support the --all
323 option.
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400324
325 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400326 executor: executor object for running pldmtool
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400327
328 """
329 try:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400330 for record_handle, pdr in get_all_pdrs_at_once(executor):
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400331 yield record_handle, pdr
332 return
333 except PLDMToolError as e:
334 # No support for the -a option
335 if e.get_status() != 106:
336 raise
337 except json.decoder.JSONDecodeError as e:
338 # Some versions of pldmtool don't print valid json documents with -a
339 if e.msg != "Extra data":
340 raise
341
Brad Bishop7dc416f2021-10-20 11:45:08 -0400342 for record_handle, pdr in get_pdrs_one_at_a_time(executor):
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400343 yield record_handle, pdr
344
345
Brad Bishop7dc416f2021-10-20 11:45:08 -0400346def fetch_pdrs_from_bmc(executor):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530347
Brad Bishop7dc416f2021-10-20 11:45:08 -0400348 """ This is the core function that would fire the getPDR pldmtool command
349 and it then agreegates the data received from all the calls into the
350 respective dictionaries based on the PDR Type.
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530351
352 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400353 executor: executor object for running pldmtool
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530354
355 """
356
357 entity_association_pdr = {}
358 state_sensor_pdr = {}
359 state_effecter_pdr = {}
360 state_effecter_pdr = {}
361 numeric_pdr = {}
362 fru_record_set_pdr = {}
363 tl_pdr = {}
Brad Bishop7dc416f2021-10-20 11:45:08 -0400364 for handle_number, my_dic in get_pdrs(executor):
Brad Bishop81c04512021-10-20 12:43:55 -0400365 if sys.stdout.isatty():
366 sys.stdout.write(
367 "Fetching PDR's from BMC : %8d\r" % (handle_number))
368 sys.stdout.flush()
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530369 if my_dic["PDRType"] == "Entity Association PDR":
370 entity_association_pdr[handle_number] = my_dic
371 if my_dic["PDRType"] == "State Sensor PDR":
372 state_sensor_pdr[handle_number] = my_dic
373 if my_dic["PDRType"] == "State Effecter PDR":
374 state_effecter_pdr[handle_number] = my_dic
375 if my_dic["PDRType"] == "FRU Record Set PDR":
376 fru_record_set_pdr[handle_number] = my_dic
377 if my_dic["PDRType"] == "Terminus Locator PDR":
378 tl_pdr[handle_number] = my_dic
379 if my_dic["PDRType"] == "Numeric Effecter PDR":
380 numeric_pdr[handle_number] = my_dic
Brad Bishop7dc416f2021-10-20 11:45:08 -0400381 executor.close()
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530382
383 total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
384 len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
385 len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
386 print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
387 print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
388 print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
389 print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
390 print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
391 print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
392 print("Number of Entity Association PDR's : ",
393 len(entity_association_pdr.keys()))
394 return (entity_association_pdr, state_sensor_pdr,
395 state_effecter_pdr, len(fru_record_set_pdr.keys()))
396
397
398def main():
399
400 """ Create a summary table capturing the information of all the PDR's
401 from the BMC & also create a diagram that captures the entity
402 association hierarchy."""
403
404 parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
Brad Bishopc8906372021-10-19 15:44:38 -0400405 parser.add_argument('--bmc', type=str, help="BMC IPAddress/BMC Hostname")
Brad Bishope0c55c82021-10-12 17:14:07 -0400406 parser.add_argument('--user', type=str, help="BMC username")
Brad Bishopc8906372021-10-19 15:44:38 -0400407 parser.add_argument('--password', type=str, help="BMC Password")
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530408 parser.add_argument('--port', type=int, help="BMC SSH port",
409 default=22)
410 args = parser.parse_args()
Brad Bishop9a8192d2021-10-04 19:58:11 -0400411
Brad Bishop9e0265b2021-10-04 20:14:25 -0400412 extra_cfg = {}
Brad Bishopc8906372021-10-19 15:44:38 -0400413 if args.bmc:
414 try:
415 with open(os.path.expanduser("~/.ssh/config")) as f:
416 ssh_config = paramiko.SSHConfig()
417 ssh_config.parse(f)
418 host_config = ssh_config.lookup(args.bmc)
419 if host_config:
420 if 'hostname' in host_config:
421 args.bmc = host_config['hostname']
422 if 'user' in host_config and args.user is None:
423 args.user = host_config['user']
424 if 'proxycommand' in host_config:
425 extra_cfg['sock'] = paramiko.ProxyCommand(
426 host_config['proxycommand'])
427 except FileNotFoundError:
428 pass
Brad Bishop9a8192d2021-10-04 19:58:11 -0400429
Brad Bishopc8906372021-10-19 15:44:38 -0400430 executor = ParamikoExecutor(
431 args.bmc, args.user, args.password, args.port, **extra_cfg)
432 elif shutil.which('pldmtool'):
433 executor = SubprocessExecutor()
434 else:
435 sys.exit("Can't find any PDRs: specify remote BMC with --bmc or "
436 "install pldmtool.")
437
Brad Bishop70ac60b2021-10-04 18:48:43 -0400438 association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
Brad Bishop7dc416f2021-10-20 11:45:08 -0400439 fetch_pdrs_from_bmc(executor)
Brad Bishop70ac60b2021-10-04 18:48:43 -0400440 draw_entity_associations(association_pdr, counter)
441 prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530442
443
444if __name__ == "__main__":
445 main()