blob: 442d607a81e1452aff4b4d79afa866945044f047 [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):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530365 sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
366 sys.stdout.flush()
367 if my_dic["PDRType"] == "Entity Association PDR":
368 entity_association_pdr[handle_number] = my_dic
369 if my_dic["PDRType"] == "State Sensor PDR":
370 state_sensor_pdr[handle_number] = my_dic
371 if my_dic["PDRType"] == "State Effecter PDR":
372 state_effecter_pdr[handle_number] = my_dic
373 if my_dic["PDRType"] == "FRU Record Set PDR":
374 fru_record_set_pdr[handle_number] = my_dic
375 if my_dic["PDRType"] == "Terminus Locator PDR":
376 tl_pdr[handle_number] = my_dic
377 if my_dic["PDRType"] == "Numeric Effecter PDR":
378 numeric_pdr[handle_number] = my_dic
Brad Bishop7dc416f2021-10-20 11:45:08 -0400379 executor.close()
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530380
381 total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
382 len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
383 len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
384 print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
385 print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
386 print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
387 print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
388 print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
389 print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
390 print("Number of Entity Association PDR's : ",
391 len(entity_association_pdr.keys()))
392 return (entity_association_pdr, state_sensor_pdr,
393 state_effecter_pdr, len(fru_record_set_pdr.keys()))
394
395
396def main():
397
398 """ Create a summary table capturing the information of all the PDR's
399 from the BMC & also create a diagram that captures the entity
400 association hierarchy."""
401
402 parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
Brad Bishopc8906372021-10-19 15:44:38 -0400403 parser.add_argument('--bmc', type=str, help="BMC IPAddress/BMC Hostname")
Brad Bishope0c55c82021-10-12 17:14:07 -0400404 parser.add_argument('--user', type=str, help="BMC username")
Brad Bishopc8906372021-10-19 15:44:38 -0400405 parser.add_argument('--password', type=str, help="BMC Password")
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530406 parser.add_argument('--port', type=int, help="BMC SSH port",
407 default=22)
408 args = parser.parse_args()
Brad Bishop9a8192d2021-10-04 19:58:11 -0400409
Brad Bishop9e0265b2021-10-04 20:14:25 -0400410 extra_cfg = {}
Brad Bishopc8906372021-10-19 15:44:38 -0400411 if args.bmc:
412 try:
413 with open(os.path.expanduser("~/.ssh/config")) as f:
414 ssh_config = paramiko.SSHConfig()
415 ssh_config.parse(f)
416 host_config = ssh_config.lookup(args.bmc)
417 if host_config:
418 if 'hostname' in host_config:
419 args.bmc = host_config['hostname']
420 if 'user' in host_config and args.user is None:
421 args.user = host_config['user']
422 if 'proxycommand' in host_config:
423 extra_cfg['sock'] = paramiko.ProxyCommand(
424 host_config['proxycommand'])
425 except FileNotFoundError:
426 pass
Brad Bishop9a8192d2021-10-04 19:58:11 -0400427
Brad Bishopc8906372021-10-19 15:44:38 -0400428 executor = ParamikoExecutor(
429 args.bmc, args.user, args.password, args.port, **extra_cfg)
430 elif shutil.which('pldmtool'):
431 executor = SubprocessExecutor()
432 else:
433 sys.exit("Can't find any PDRs: specify remote BMC with --bmc or "
434 "install pldmtool.")
435
Brad Bishop70ac60b2021-10-04 18:48:43 -0400436 association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
Brad Bishop7dc416f2021-10-20 11:45:08 -0400437 fetch_pdrs_from_bmc(executor)
Brad Bishop70ac60b2021-10-04 18:48:43 -0400438 draw_entity_associations(association_pdr, counter)
439 prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530440
441
442if __name__ == "__main__":
443 main()