1#!/usr/bin/env python3
2
3r"""
4This module contains functions having to do with redfish path walking.
5"""
6
7import json
8import os
9import subprocess
10
11ERROR_RESPONSE = {
12    "404": "Response Error: status_code: 404 -- Not Found",
13    "500": "Response Error: status_code: 500 -- Internal Server Error",
14}
15
16# Variable to hold enumerated data.
17result = {}
18
19# Variable to hold the pending list of resources for which enumeration.
20# is yet to be obtained.
21pending_enumeration = set()
22
23
24def execute_redfish_cmd(parms, json_type="json"):
25    r"""
26    Run CLI standard redfish tool.
27
28    Description of variable:
29    parms_string         Command to execute from the current SHELL.
30    quiet                do not print tool error message if True
31    """
32    resp = subprocess.run(
33        [parms],
34        stdout=subprocess.PIPE,
35        stderr=subprocess.PIPE,
36        shell=True,
37        universal_newlines=True,
38    )
39
40    if resp.stderr:
41        print("\n\t\tERROR with %s " % parms)
42        print("\t\t" + resp.stderr)
43        return resp.stderr
44    elif json_type == "json":
45        json_data = json.loads(resp.stdout)
46        return json_data
47    else:
48        return resp.stdout
49
50
51def enumerate_request(hostname, username, password, url, return_json="json"):
52    r"""
53    Perform a GET enumerate request and return available resource paths.
54
55    Description of argument(s):
56    url               URI resource absolute path (e.g.
57                      "/redfish/v1/SessionService/Sessions").
58    return_json       Indicates whether the result should be
59                      returned as a json string or as a
60                      dictionary.
61    """
62    parms = (
63        "redfishtool -u "
64        + username
65        + " -p "
66        + password
67        + " -r "
68        + hostname
69        + " -S Always raw GET "
70    )
71
72    pending_enumeration.add(url)
73
74    # Variable having resources for which enumeration is completed.
75    enumerated_resources = set()
76
77    resources_to_be_enumerated = (url,)
78
79    while resources_to_be_enumerated:
80        for resource in resources_to_be_enumerated:
81            # JsonSchemas, SessionService or URLs containing # are not
82            # required in enumeration.
83            # Example: '/redfish/v1/JsonSchemas/' and sub resources.
84            #          '/redfish/v1/SessionService'
85            #          '/redfish/v1/Managers/${MANAGER_ID}#/Oem'
86            if (
87                ("JsonSchemas" in resource)
88                or ("SessionService" in resource)
89                or ("PostCodes" in resource)
90                or ("Registries" in resource)
91                or ("#" in resource)
92            ):
93                continue
94
95            response = execute_redfish_cmd(parms + resource)
96            # Enumeration is done for available resources ignoring the
97            # ones for which response is not obtained.
98            if "Error getting response" in response:
99                continue
100
101            walk_nested_dict(response, url=resource)
102
103        enumerated_resources.update(set(resources_to_be_enumerated))
104        resources_to_be_enumerated = tuple(
105            pending_enumeration - enumerated_resources
106        )
107
108    if return_json == "json":
109        return json.dumps(
110            result, sort_keys=True, indent=4, separators=(",", ": ")
111        )
112    else:
113        return result
114
115
116def walk_nested_dict(data, url=""):
117    r"""
118    Parse through the nested dictionary and get the resource id paths.
119
120    Description of argument(s):
121    data    Nested dictionary data from response message.
122    url     Resource for which the response is obtained in data.
123    """
124    url = url.rstrip("/")
125
126    for key, value in data.items():
127        # Recursion if nested dictionary found.
128        if isinstance(value, dict):
129            walk_nested_dict(value)
130        else:
131            # Value contains a list of dictionaries having member data.
132            if "Members" == key:
133                if isinstance(value, list):
134                    for memberDict in value:
135                        if isinstance(memberDict, str):
136                            pending_enumeration.add(memberDict)
137                        else:
138                            pending_enumeration.add(memberDict["@odata.id"])
139
140            if "@odata.id" == key:
141                value = value.rstrip("/")
142                # Data for the given url.
143                if value == url:
144                    result[url] = data
145                # Data still needs to be looked up,
146                else:
147                    pending_enumeration.add(value)
148
149
150def get_key_value_nested_dict(data, key):
151    r"""
152    Parse through the nested dictionary and get the searched key value.
153
154    Description of argument(s):
155    data    Nested dictionary data from response message.
156    key     Search dictionary key element.
157    """
158
159    for k, v in data.items():
160        if isinstance(v, dict):
161            get_key_value_nested_dict(v, key)
162
163        if k == key:
164            target_list.append(v)
165