blob: 4b474e0e6c25a13d4068f9f3adf87bf4fd0fd1cf [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 Bishop241e0682021-10-19 15:03:39 -040016class Process:
17 """ Interface definition for interacting with a process created by an
18 Executor."""
19
20 def __init__(self, stdout, stderr):
21 """ Construct a Process object. Process object clients can read the
22 process stdout and stderr with os.read(), and can wait for the
23 process to exit.
24
25 Parameters:
26 stdout: os.read()able stream representing stdout
27 stderr: os.read()able stream representing stderr
28 """
29
30 self.stdout = stdout
31 self.stderr = stderr
32
33 def wait(self):
34 """ Wait for the process to finish, and return its exit status."""
35
36 raise NotImplementedError
37
38
Brad Bishop7dc416f2021-10-20 11:45:08 -040039class Executor:
40 """ Interface definition for interacting with executors. An executor is an
41 object that can run a program."""
Manojkiran Edab8cc3252021-05-24 11:29:36 +053042
Brad Bishop7dc416f2021-10-20 11:45:08 -040043 def exec_command(self, cmd):
44 raise NotImplementedError
Manojkiran Edab8cc3252021-05-24 11:29:36 +053045
Brad Bishop7dc416f2021-10-20 11:45:08 -040046 def close(self):
47 pass
Manojkiran Edab8cc3252021-05-24 11:29:36 +053048
Manojkiran Edab8cc3252021-05-24 11:29:36 +053049
Brad Bishop241e0682021-10-19 15:03:39 -040050class ParamikoProcess(Process):
51 """ Concrete implementation of the Process interface that adapts Paramiko
52 interfaces to the Process interface requirements."""
53
54 def __init__(self, stdout, stderr):
55 super(ParamikoProcess, self).__init__(stdout, stderr)
56
57 def wait(self):
58 return self.stderr.channel.recv_exit_status()
59
60
Brad Bishop7dc416f2021-10-20 11:45:08 -040061class ParamikoExecutor(Executor):
62 """ Concrete implementation of the Executor interface that uses
63 Paramiko to connect to a remote BMC to run the program."""
64
65 def __init__(self, hostname, uname, passwd, port, **kw):
66 """ This function is responsible for connecting to the BMC via
67 ssh and returning an executor object.
68
69 Parameters:
70 hostname: hostname/IP address of BMC
71 uname: ssh username of BMC
72 passwd: ssh password of BMC
73 port: ssh port of BMC
74 """
75
76 super(ParamikoExecutor, self).__init__()
77 self.client = paramiko.SSHClient()
78 self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
79 self.client.connect(
80 hostname, username=uname, password=passwd, port=port, **kw)
81
82 def exec_command(self, cmd):
Brad Bishop241e0682021-10-19 15:03:39 -040083 _, stdout, stderr = self.client.exec_command(cmd)
84 return ParamikoProcess(stdout, stderr)
Brad Bishop7dc416f2021-10-20 11:45:08 -040085
86 def close(self):
87 self.client.close()
Manojkiran Edab8cc3252021-05-24 11:29:36 +053088
89
90def prepare_summary_report(state_sensor_pdr, state_effecter_pdr):
91
92 """ This function is responsible to parse the state sensor pdr
93 and the state effecter pdr dictionaries and creating the
94 summary table.
95
96 Parameters:
97 state_sensor_pdr: list of state sensor pdrs
98 state_effecter_pdr: list of state effecter pdrs
99
100 """
101
102 summary_table = []
103 headers = ["sensor_id", "entity_type", "state_set", "states"]
104 summary_table.append(headers)
105 for value in state_sensor_pdr.values():
106 summary_record = []
107 sensor_possible_states = ''
108 for sensor_state in value["possibleStates[0]"]:
109 sensor_possible_states += sensor_state+"\n"
110 summary_record.extend([value["sensorID"], value["entityType"],
111 value["stateSetID[0]"],
112 sensor_possible_states])
113 summary_table.append(summary_record)
114 print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
115 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
116
117 summary_table = []
118 headers = ["effecter_id", "entity_type", "state_set", "states"]
119 summary_table.append(headers)
120 for value in state_effecter_pdr.values():
121 summary_record = []
122 effecter_possible_states = ''
123 for state in value["possibleStates[0]"]:
124 effecter_possible_states += state+"\n"
125 summary_record.extend([value["effecterID"], value["entityType"],
126 value["stateSetID[0]"],
127 effecter_possible_states])
128 summary_table.append(summary_record)
129 print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
130
131
132def draw_entity_associations(pdr, counter):
133
134 """ This function is responsible to create a picture that captures
135 the entity association hierarchy based on the entity association
136 PDR's received from the BMC.
137
138 Parameters:
139 pdr: list of entity association PDR's
140 counter: variable to capture the count of PDR's to unflatten
141 the tree
142
143 """
144
145 dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1',
146 'style': 'filled'})
147 dot.attr(label=r'\n\nEntity Relation Diagram < ' +
148 str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n')
149 dot.attr(fontsize='20')
150 edge_list = []
151 for value in pdr.values():
152 parentnode = str(value["containerEntityType"]) + \
153 str(value["containerEntityInstanceNumber"])
154 dot.node(hashlib.md5((parentnode +
155 str(value["containerEntityContainerID"]))
156 .encode()).hexdigest(), parentnode)
157
158 for i in range(1, value["containedEntityCount"]+1):
159 childnode = str(value[f"containedEntityType[{i}]"]) + \
160 str(value[f"containedEntityInstanceNumber[{i}]"])
161 cid = str(value[f"containedEntityContainerID[{i}]"])
162 dot.node(hashlib.md5((childnode + cid)
163 .encode()).hexdigest(), childnode)
164
165 if[hashlib.md5((parentnode +
166 str(value["containerEntityContainerID"]))
167 .encode()).hexdigest(),
168 hashlib.md5((childnode + cid)
169 .encode()).hexdigest()] not in edge_list:
170 edge_list.append([hashlib.md5((parentnode +
171 str(value["containerEntityContainerID"]))
172 .encode()).hexdigest(),
173 hashlib.md5((childnode + cid)
174 .encode()).hexdigest()])
175 dot.edge(hashlib.md5((parentnode +
176 str(value["containerEntityContainerID"]))
177 .encode()).hexdigest(),
178 hashlib.md5((childnode + cid).encode()).hexdigest())
179 unflattentree = dot.unflatten(stagger=(round(counter/3)))
180 unflattentree.render(filename='entity_association_' +
181 str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")),
182 view=False, cleanup=True, format='pdf')
183
184
Brad Bishop260f75a2021-10-15 12:10:29 -0400185class PLDMToolError(Exception):
186 """ Exception class intended to be used to hold pldmtool invocation failure
187 information such as exit status and stderr.
188
189 """
190
191 def __init__(self, status, stderr):
192 msg = "pldmtool failed with exit status {}.\n".format(status)
193 msg += "stderr: \n\n{}".format(stderr)
194 super(PLDMToolError, self).__init__(msg)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400195 self.status = status
196
197 def get_status(self):
198 return self.status
Brad Bishop260f75a2021-10-15 12:10:29 -0400199
200
Brad Bishop241e0682021-10-19 15:03:39 -0400201def process_pldmtool_output(process):
Brad Bishop260f75a2021-10-15 12:10:29 -0400202 """ Ensure pldmtool runs without error and if it does fail, detect that and
203 show the pldmtool exit status and it's stderr.
204
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400205 A simpler implementation would just wait for the pldmtool exit status
206 prior to attempting to decode it's stdout. Instead, optimize for the
207 no error case and allow the json decoder to consume pldmtool stdout as
208 soon as it is available (in parallel). This results in the following
209 error scenarios:
210 - pldmtool fails and the decoder fails
211 Ignore the decoder fail and throw PLDMToolError.
212 - pldmtool fails and the decoder doesn't fail
213 Throw PLDMToolError.
214 - pldmtool doesn't fail and the decoder does fail
215 This is a pldmtool bug - re-throw the decoder error.
216
Brad Bishop260f75a2021-10-15 12:10:29 -0400217 Parameters:
Brad Bishop241e0682021-10-19 15:03:39 -0400218 process: A Process object providing process control functions like
219 wait, and access functions such as reading stdout and
220 stderr.
Brad Bishop260f75a2021-10-15 12:10:29 -0400221
222 """
223
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400224 status = 0
225 try:
Brad Bishop241e0682021-10-19 15:03:39 -0400226 data = json.load(process.stdout)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400227 # it's unlikely, but possible, that pldmtool failed but still wrote a
228 # valid json document - so check for that.
Brad Bishop241e0682021-10-19 15:03:39 -0400229 status = process.wait()
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400230 if status == 0:
231 return data
232 except json.decoder.JSONDecodeError:
233 # pldmtool wrote an invalid json document. Check to see if it had
234 # non-zero exit status.
Brad Bishop241e0682021-10-19 15:03:39 -0400235 status = process.wait()
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400236 if status == 0:
237 # pldmtool didn't have non zero exit status, so it wrote an invalid
238 # json document and the JSONDecodeError is the correct error.
239 raise
Brad Bishop260f75a2021-10-15 12:10:29 -0400240
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400241 # pldmtool had a non-zero exit status, so throw an error for that, possibly
242 # discarding a spurious JSONDecodeError exception.
Brad Bishop241e0682021-10-19 15:03:39 -0400243 raise PLDMToolError(status, "".join(process.stderr))
Brad Bishop260f75a2021-10-15 12:10:29 -0400244
245
Brad Bishop7dc416f2021-10-20 11:45:08 -0400246def get_pdrs_one_at_a_time(executor):
247 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
248 the PDR repository.
Brad Bishop91523132021-10-14 19:25:37 -0400249
250 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400251 executor: executor object for running pldmtool
Brad Bishop91523132021-10-14 19:25:37 -0400252
253 """
254
255 command_fmt = 'pldmtool platform getpdr -d {}'
256 record_handle = 0
257 while True:
Brad Bishop241e0682021-10-19 15:03:39 -0400258 process = executor.exec_command(command_fmt.format(str(record_handle)))
259 pdr = process_pldmtool_output(process)
Brad Bishop91523132021-10-14 19:25:37 -0400260 yield record_handle, pdr
261 record_handle = pdr["nextRecordHandle"]
262 if record_handle == 0:
263 break
264
265
Brad Bishop7dc416f2021-10-20 11:45:08 -0400266def get_all_pdrs_at_once(executor):
267 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
268 the PDR repository. Use pldmtool platform getpdr --all.
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400269
270 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400271 executor: executor object for running pldmtool
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400272
273 """
274
Brad Bishop241e0682021-10-19 15:03:39 -0400275 process = executor.exec_command('pldmtool platform getpdr -a')
276 all_pdrs = process_pldmtool_output(process)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400277
278 # Explicitly request record 0 to find out what the real first record is.
Brad Bishop241e0682021-10-19 15:03:39 -0400279 process = executor.exec_command('pldmtool platform getpdr -d 0')
280 pdr_0 = process_pldmtool_output(process)
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400281 record_handle = pdr_0["recordHandle"]
282
283 while True:
284 for pdr in all_pdrs:
285 if pdr["recordHandle"] == record_handle:
286 yield record_handle, pdr
287 record_handle = pdr["nextRecordHandle"]
288 if record_handle == 0:
289 return
290 raise RuntimeError(
291 "Dangling reference to record {}".format(record_handle))
292
293
Brad Bishop7dc416f2021-10-20 11:45:08 -0400294def get_pdrs(executor):
295 """ Using pldmtool, generate (record handle, PDR) tuples for each record in
296 the PDR repository. Use pldmtool platform getpdr --all or fallback on
297 getting them one at a time if pldmtool doesn't support the --all
298 option.
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400299
300 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400301 executor: executor object for running pldmtool
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400302
303 """
304 try:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400305 for record_handle, pdr in get_all_pdrs_at_once(executor):
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400306 yield record_handle, pdr
307 return
308 except PLDMToolError as e:
309 # No support for the -a option
310 if e.get_status() != 106:
311 raise
312 except json.decoder.JSONDecodeError as e:
313 # Some versions of pldmtool don't print valid json documents with -a
314 if e.msg != "Extra data":
315 raise
316
Brad Bishop7dc416f2021-10-20 11:45:08 -0400317 for record_handle, pdr in get_pdrs_one_at_a_time(executor):
Brad Bishop98cb3ef2021-10-15 14:31:59 -0400318 yield record_handle, pdr
319
320
Brad Bishop7dc416f2021-10-20 11:45:08 -0400321def fetch_pdrs_from_bmc(executor):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530322
Brad Bishop7dc416f2021-10-20 11:45:08 -0400323 """ This is the core function that would fire the getPDR pldmtool command
324 and it then agreegates the data received from all the calls into the
325 respective dictionaries based on the PDR Type.
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530326
327 Parameters:
Brad Bishop7dc416f2021-10-20 11:45:08 -0400328 executor: executor object for running pldmtool
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530329
330 """
331
332 entity_association_pdr = {}
333 state_sensor_pdr = {}
334 state_effecter_pdr = {}
335 state_effecter_pdr = {}
336 numeric_pdr = {}
337 fru_record_set_pdr = {}
338 tl_pdr = {}
Brad Bishop7dc416f2021-10-20 11:45:08 -0400339 for handle_number, my_dic in get_pdrs(executor):
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530340 sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
341 sys.stdout.flush()
342 if my_dic["PDRType"] == "Entity Association PDR":
343 entity_association_pdr[handle_number] = my_dic
344 if my_dic["PDRType"] == "State Sensor PDR":
345 state_sensor_pdr[handle_number] = my_dic
346 if my_dic["PDRType"] == "State Effecter PDR":
347 state_effecter_pdr[handle_number] = my_dic
348 if my_dic["PDRType"] == "FRU Record Set PDR":
349 fru_record_set_pdr[handle_number] = my_dic
350 if my_dic["PDRType"] == "Terminus Locator PDR":
351 tl_pdr[handle_number] = my_dic
352 if my_dic["PDRType"] == "Numeric Effecter PDR":
353 numeric_pdr[handle_number] = my_dic
Brad Bishop7dc416f2021-10-20 11:45:08 -0400354 executor.close()
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530355
356 total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
357 len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
358 len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
359 print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
360 print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
361 print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
362 print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
363 print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
364 print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
365 print("Number of Entity Association PDR's : ",
366 len(entity_association_pdr.keys()))
367 return (entity_association_pdr, state_sensor_pdr,
368 state_effecter_pdr, len(fru_record_set_pdr.keys()))
369
370
371def main():
372
373 """ Create a summary table capturing the information of all the PDR's
374 from the BMC & also create a diagram that captures the entity
375 association hierarchy."""
376
377 parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
378 parser.add_argument('--bmc', type=str, required=True,
379 help="BMC IPAddress/BMC Hostname")
Brad Bishope0c55c82021-10-12 17:14:07 -0400380 parser.add_argument('--user', type=str, help="BMC username")
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530381 parser.add_argument('--password', type=str, required=True,
382 help="BMC Password")
383 parser.add_argument('--port', type=int, help="BMC SSH port",
384 default=22)
385 args = parser.parse_args()
Brad Bishop9a8192d2021-10-04 19:58:11 -0400386
Brad Bishop9e0265b2021-10-04 20:14:25 -0400387 extra_cfg = {}
Brad Bishop9a8192d2021-10-04 19:58:11 -0400388 try:
389 with open(os.path.expanduser("~/.ssh/config")) as f:
390 ssh_config = paramiko.SSHConfig()
391 ssh_config.parse(f)
392 host_config = ssh_config.lookup(args.bmc)
393 if host_config:
394 if 'hostname' in host_config:
395 args.bmc = host_config['hostname']
396 if 'user' in host_config and args.user is None:
397 args.user = host_config['user']
Brad Bishop9e0265b2021-10-04 20:14:25 -0400398 if 'proxycommand' in host_config:
399 extra_cfg['sock'] = paramiko.ProxyCommand(
400 host_config['proxycommand'])
Brad Bishop9a8192d2021-10-04 19:58:11 -0400401 except FileNotFoundError:
402 pass
403
Brad Bishop7dc416f2021-10-20 11:45:08 -0400404 executor = ParamikoExecutor(
Brad Bishop9e0265b2021-10-04 20:14:25 -0400405 args.bmc, args.user, args.password, args.port, **extra_cfg)
Brad Bishop70ac60b2021-10-04 18:48:43 -0400406 association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
Brad Bishop7dc416f2021-10-20 11:45:08 -0400407 fetch_pdrs_from_bmc(executor)
Brad Bishop70ac60b2021-10-04 18:48:43 -0400408 draw_entity_associations(association_pdr, counter)
409 prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
Manojkiran Edab8cc3252021-05-24 11:29:36 +0530410
411
412if __name__ == "__main__":
413 main()