1#!/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
13
14
15def connect_to_bmc(hostname, uname, passwd, port):
16
17    """ This function is responsible to connect to the BMC via
18        ssh and returns a client object.
19
20        Parameters:
21            hostname: hostname/IP address of BMC
22            uname: ssh username of BMC
23            passwd: ssh password of BMC
24            port: ssh port of BMC
25
26    """
27
28    client = paramiko.SSHClient()
29    client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
30    client.connect(hostname, username=uname, password=passwd, port=port)
31    return client
32
33
34def prepare_summary_report(state_sensor_pdr, state_effecter_pdr):
35
36    """ This function is responsible to parse the state sensor pdr
37        and the state effecter pdr dictionaries and creating the
38        summary table.
39
40        Parameters:
41            state_sensor_pdr: list of state sensor pdrs
42            state_effecter_pdr: list of state effecter pdrs
43
44    """
45
46    summary_table = []
47    headers = ["sensor_id", "entity_type", "state_set", "states"]
48    summary_table.append(headers)
49    for value in state_sensor_pdr.values():
50        summary_record = []
51        sensor_possible_states = ''
52        for sensor_state in value["possibleStates[0]"]:
53            sensor_possible_states += sensor_state+"\n"
54        summary_record.extend([value["sensorID"], value["entityType"],
55                               value["stateSetID[0]"],
56                               sensor_possible_states])
57        summary_table.append(summary_record)
58    print("Created at : ", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
59    print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
60
61    summary_table = []
62    headers = ["effecter_id", "entity_type", "state_set", "states"]
63    summary_table.append(headers)
64    for value in state_effecter_pdr.values():
65        summary_record = []
66        effecter_possible_states = ''
67        for state in value["possibleStates[0]"]:
68            effecter_possible_states += state+"\n"
69        summary_record.extend([value["effecterID"], value["entityType"],
70                               value["stateSetID[0]"],
71                               effecter_possible_states])
72        summary_table.append(summary_record)
73    print(tabulate(summary_table, tablefmt="fancy_grid", headers="firstrow"))
74
75
76def draw_entity_associations(pdr, counter):
77
78    """ This function is responsible to create a picture that captures
79        the entity association hierarchy based on the entity association
80        PDR's received from the BMC.
81
82        Parameters:
83            pdr: list of entity association PDR's
84            counter: variable to capture the count of PDR's to unflatten
85                     the tree
86
87    """
88
89    dot = Digraph('entity_hierarchy', node_attr={'color': 'lightblue1',
90                                                 'style': 'filled'})
91    dot.attr(label=r'\n\nEntity Relation Diagram < ' +
92             str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))+'>\n')
93    dot.attr(fontsize='20')
94    edge_list = []
95    for value in pdr.values():
96        parentnode = str(value["containerEntityType"]) + \
97                     str(value["containerEntityInstanceNumber"])
98        dot.node(hashlib.md5((parentnode +
99                              str(value["containerEntityContainerID"]))
100                             .encode()).hexdigest(), parentnode)
101
102        for i in range(1, value["containedEntityCount"]+1):
103            childnode = str(value[f"containedEntityType[{i}]"]) + \
104                        str(value[f"containedEntityInstanceNumber[{i}]"])
105            cid = str(value[f"containedEntityContainerID[{i}]"])
106            dot.node(hashlib.md5((childnode + cid)
107                                 .encode()).hexdigest(), childnode)
108
109            if[hashlib.md5((parentnode +
110                            str(value["containerEntityContainerID"]))
111                           .encode()).hexdigest(),
112               hashlib.md5((childnode + cid)
113                           .encode()).hexdigest()] not in edge_list:
114                edge_list.append([hashlib.md5((parentnode +
115                                  str(value["containerEntityContainerID"]))
116                                              .encode()).hexdigest(),
117                                  hashlib.md5((childnode + cid)
118                                              .encode()).hexdigest()])
119                dot.edge(hashlib.md5((parentnode +
120                                      str(value["containerEntityContainerID"]))
121                                     .encode()).hexdigest(),
122                         hashlib.md5((childnode + cid).encode()).hexdigest())
123    unflattentree = dot.unflatten(stagger=(round(counter/3)))
124    unflattentree.render(filename='entity_association_' +
125                         str(datetime.now().strftime("%Y-%m-%d_%H-%M-%S")),
126                         view=False, cleanup=True, format='pdf')
127
128
129class PLDMToolError(Exception):
130    """ Exception class intended to be used to hold pldmtool invocation failure
131        information such as exit status and stderr.
132
133    """
134
135    def __init__(self, status, stderr):
136        msg = "pldmtool failed with exit status {}.\n".format(status)
137        msg += "stderr: \n\n{}".format(stderr)
138        super(PLDMToolError, self).__init__(msg)
139
140
141def process_pldmtool_output(stdout_channel, stderr_channel):
142    """ Ensure pldmtool runs without error and if it does fail, detect that and
143        show the pldmtool exit status and it's stderr.
144
145        Parameters:
146            stdout_channel: file-like stdout channel
147            stderr_channel: file-like stderr channel
148
149    """
150
151    status = stderr_channel.channel.recv_exit_status()
152    if status == 0:
153        return json.load(stdout_channel)
154
155    raise PLDMToolError(status, "".join(stderr_channel))
156
157
158def get_pdrs(client):
159    """ Using pldmtool over SSH, generate (record handle, PDR) tuples for each
160        record in the PDR repository.
161
162        Parameters:
163            client: paramiko ssh client object
164
165    """
166
167    command_fmt = 'pldmtool platform getpdr -d {}'
168    record_handle = 0
169    while True:
170        output = client.exec_command(command_fmt.format(str(record_handle)))
171        _, stdout, stderr = output
172        pdr = process_pldmtool_output(stdout, stderr)
173        yield record_handle, pdr
174        record_handle = pdr["nextRecordHandle"]
175        if record_handle == 0:
176            break
177
178
179def fetch_pdrs_from_bmc(client):
180
181    """ This is the core function that would use the existing ssh connection
182        object to connect to BMC and fire the getPDR pldmtool command
183        and it then agreegates the data received from all the calls into
184        the respective dictionaries based on the PDR Type.
185
186        Parameters:
187            client: paramiko ssh client object
188
189    """
190
191    entity_association_pdr = {}
192    state_sensor_pdr = {}
193    state_effecter_pdr = {}
194    state_effecter_pdr = {}
195    numeric_pdr = {}
196    fru_record_set_pdr = {}
197    tl_pdr = {}
198    for handle_number, my_dic in get_pdrs(client):
199        sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
200        sys.stdout.flush()
201        if my_dic["PDRType"] == "Entity Association PDR":
202            entity_association_pdr[handle_number] = my_dic
203        if my_dic["PDRType"] == "State Sensor PDR":
204            state_sensor_pdr[handle_number] = my_dic
205        if my_dic["PDRType"] == "State Effecter PDR":
206            state_effecter_pdr[handle_number] = my_dic
207        if my_dic["PDRType"] == "FRU Record Set PDR":
208            fru_record_set_pdr[handle_number] = my_dic
209        if my_dic["PDRType"] == "Terminus Locator PDR":
210            tl_pdr[handle_number] = my_dic
211        if my_dic["PDRType"] == "Numeric Effecter PDR":
212            numeric_pdr[handle_number] = my_dic
213    client.close()
214
215    total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
216        len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
217        len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
218    print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
219    print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
220    print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
221    print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
222    print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
223    print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
224    print("Number of Entity Association PDR's : ",
225          len(entity_association_pdr.keys()))
226    return (entity_association_pdr, state_sensor_pdr,
227            state_effecter_pdr, len(fru_record_set_pdr.keys()))
228
229
230def main():
231
232    """ Create a summary table capturing the information of all the PDR's
233        from the BMC & also create a diagram that captures the entity
234        association hierarchy."""
235
236    parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
237    parser.add_argument('--bmc', type=str, required=True,
238                        help="BMC IPAddress/BMC Hostname")
239    parser.add_argument('--user', type=str, help="BMC username")
240    parser.add_argument('--password', type=str, required=True,
241                        help="BMC Password")
242    parser.add_argument('--port', type=int, help="BMC SSH port",
243                        default=22)
244    args = parser.parse_args()
245    client = connect_to_bmc(args.bmc, args.user, args.password, args.port)
246    association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
247        fetch_pdrs_from_bmc(client)
248    draw_entity_associations(association_pdr, counter)
249    prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
250
251
252if __name__ == "__main__":
253    main()
254