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
129def fetch_pdrs_from_bmc(client):
130
131    """ This is the core function that would use the existing ssh connection
132        object to connect to BMC and fire the getPDR pldmtool command
133        and it then agreegates the data received from all the calls into
134        the respective dictionaries based on the PDR Type.
135
136        Parameters:
137            client: paramiko ssh client object
138
139    """
140
141    entity_association_pdr = {}
142    state_sensor_pdr = {}
143    state_effecter_pdr = {}
144    state_effecter_pdr = {}
145    numeric_pdr = {}
146    fru_record_set_pdr = {}
147    tl_pdr = {}
148    handle_number = 0
149    while True:
150        my_str = ''
151        command = 'pldmtool platform getpdr -d ' + str(handle_number)
152        output = client.exec_command(command)
153        for line in output[1]:
154            my_str += line.strip('\n')
155        my_dic = json.loads(my_str)
156        sys.stdout.write("Fetching PDR's from BMC : %8d\r" % (handle_number))
157        sys.stdout.flush()
158        if my_dic["PDRType"] == "Entity Association PDR":
159            entity_association_pdr[handle_number] = my_dic
160        if my_dic["PDRType"] == "State Sensor PDR":
161            state_sensor_pdr[handle_number] = my_dic
162        if my_dic["PDRType"] == "State Effecter PDR":
163            state_effecter_pdr[handle_number] = my_dic
164        if my_dic["PDRType"] == "FRU Record Set PDR":
165            fru_record_set_pdr[handle_number] = my_dic
166        if my_dic["PDRType"] == "Terminus Locator PDR":
167            tl_pdr[handle_number] = my_dic
168        if my_dic["PDRType"] == "Numeric Effecter PDR":
169            numeric_pdr[handle_number] = my_dic
170        if not my_dic["nextRecordHandle"] == 0:
171            handle_number = my_dic["nextRecordHandle"]
172        else:
173            break
174    client.close()
175
176    total_pdrs = len(entity_association_pdr.keys()) + len(tl_pdr.keys()) + \
177        len(state_effecter_pdr.keys()) + len(numeric_pdr.keys()) + \
178        len(state_sensor_pdr.keys()) + len(fru_record_set_pdr.keys())
179    print("\nSuccessfully fetched " + str(total_pdrs) + " PDR\'s")
180    print("Number of FRU Record PDR's : ", len(fru_record_set_pdr.keys()))
181    print("Number of TerminusLocator PDR's : ", len(tl_pdr.keys()))
182    print("Number of State Sensor PDR's : ", len(state_sensor_pdr.keys()))
183    print("Number of State Effecter PDR's : ", len(state_effecter_pdr.keys()))
184    print("Number of Numeric Effecter PDR's : ", len(numeric_pdr.keys()))
185    print("Number of Entity Association PDR's : ",
186          len(entity_association_pdr.keys()))
187    return (entity_association_pdr, state_sensor_pdr,
188            state_effecter_pdr, len(fru_record_set_pdr.keys()))
189
190
191def main():
192
193    """ Create a summary table capturing the information of all the PDR's
194        from the BMC & also create a diagram that captures the entity
195        association hierarchy."""
196
197    parser = argparse.ArgumentParser(prog='pldm_visualise_pdrs.py')
198    parser.add_argument('--bmc', type=str, required=True,
199                        help="BMC IPAddress/BMC Hostname")
200    parser.add_argument('--user', type=str, required=True,
201                        help="BMC username")
202    parser.add_argument('--password', type=str, required=True,
203                        help="BMC Password")
204    parser.add_argument('--port', type=int, help="BMC SSH port",
205                        default=22)
206    args = parser.parse_args()
207    if args.bmc and args.password and args.user:
208        client = connect_to_bmc(args.bmc, args.user, args.password, args.port)
209        association_pdr, state_sensor_pdr, state_effecter_pdr, counter = \
210            fetch_pdrs_from_bmc(client)
211        draw_entity_associations(association_pdr, counter)
212        prepare_summary_report(state_sensor_pdr, state_effecter_pdr)
213
214
215if __name__ == "__main__":
216    main()
217