Copy enumerate_request to bmc_redfish.py

Copy enumerate_request from bmc_redfish_utils.py to bmc_redfish.py and
rename it to simply "enumerate".

Functions like "enumerate" and "list" are quite integral to BMC redfish
programming and belong in bmc_redfish.py.

Change-Id: I8e7aa9d8833ecf19194abc1e966841ac069ff5d8
Signed-off-by: Michael Walsh <micwalsh@us.ibm.com>
diff --git a/lib/bmc_redfish.py b/lib/bmc_redfish.py
index 3f1f322..835af31 100644
--- a/lib/bmc_redfish.py
+++ b/lib/bmc_redfish.py
@@ -6,6 +6,7 @@
 
 import sys
 import re
+import json
 from redfish_plus import redfish_plus
 from robot.libraries.BuiltIn import BuiltIn
 
@@ -146,3 +147,94 @@
         """
 
         return self.get_session_key(), self.get_session_location()
+
+    def enumerate(self, resource_path, return_json=1, include_dead_resources=False):
+        r"""
+        Perform a GET enumerate request and return available resource paths.
+
+        Description of argument(s):
+        resource_path               URI resource absolute path (e.g. "/redfish/v1/SessionService/Sessions").
+        return_json                 Indicates whether the result should be returned as a json string or as a
+                                    dictionary.
+        include_dead_resources      Check and return a list of dead/broken URI resources.
+        """
+
+        gp.qprint_executing(style=gp.func_line_style_short)
+        # Set quiet variable to keep subordinate get() calls quiet.
+        quiet = 1
+
+        self.__result = {}
+        # Variable to hold the pending list of resources for which enumeration is yet to be obtained.
+        self.__pending_enumeration = set()
+        self.__pending_enumeration.add(resource_path)
+
+        # Variable having resources for which enumeration is completed.
+        enumerated_resources = set()
+        dead_resources = {}
+        resources_to_be_enumerated = (resource_path,)
+        while resources_to_be_enumerated:
+            for resource in resources_to_be_enumerated:
+                # JsonSchemas, SessionService or URLs containing # are not required in enumeration.
+                # Example: '/redfish/v1/JsonSchemas/' and sub resources.
+                #          '/redfish/v1/SessionService'
+                #          '/redfish/v1/Managers/bmc#/Oem'
+                if ('JsonSchemas' in resource) or ('SessionService' in resource) or ('#' in resource):
+                    continue
+
+                self._rest_response_ = self.get(resource, valid_status_codes=[200, 404, 500])
+                # Enumeration is done for available resources ignoring the ones for which response is not
+                # obtained.
+                if self._rest_response_.status != 200:
+                    if include_dead_resources:
+                        try:
+                            dead_resources[self._rest_response_.status].append(resource)
+                        except KeyError:
+                            dead_resources[self._rest_response_.status] = [resource]
+                    continue
+                self.walk_nested_dict(self._rest_response_.dict, url=resource)
+
+            enumerated_resources.update(set(resources_to_be_enumerated))
+            resources_to_be_enumerated = tuple(self.__pending_enumeration - enumerated_resources)
+
+        if return_json:
+            if include_dead_resources:
+                return json.dumps(self.__result, sort_keys=True,
+                                  indent=4, separators=(',', ': ')), dead_resources
+            else:
+                return json.dumps(self.__result, sort_keys=True,
+                                  indent=4, separators=(',', ': '))
+        else:
+            if include_dead_resources:
+                return self.__result, dead_resources
+            else:
+                return self.__result
+
+    def walk_nested_dict(self, data, url=''):
+        r"""
+        Parse through the nested dictionary and get the resource id paths.
+
+        Description of argument(s):
+        data                        Nested dictionary data from response message.
+        url                         Resource for which the response is obtained in data.
+        """
+        url = url.rstrip('/')
+
+        for key, value in data.items():
+
+            # Recursion if nested dictionary found.
+            if isinstance(value, dict):
+                self.walk_nested_dict(value)
+            else:
+                # Value contains a list of dictionaries having member data.
+                if 'Members' == key:
+                    if isinstance(value, list):
+                        for memberDict in value:
+                            self.__pending_enumeration.add(memberDict['@odata.id'])
+                if '@odata.id' == key:
+                    value = value.rstrip('/')
+                    # Data for the given url.
+                    if value == url:
+                        self.__result[url] = data
+                    # Data still needs to be looked up,
+                    else:
+                        self.__pending_enumeration.add(value)