### # Copyright 2020 Hewlett Packard Enterprise, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. ### # -*- coding: utf-8 -*- """Utility functions for internal and external use. Contains general json navigating functions as well as some monolith utility functions.""" import copy import logging import re import sys import six if six.PY3: from functools import reduce try: from collections import Mapping except ImportError: from collections.abc import Mapping import jsonpath_rw from six import iterkeys, string_types from redfish.ris.rmc_helper import IncorrectPropValue try: # itertools ifilter compatibility for python 2 from future_builtins import filter except ImportError: # filter function provides the same functionality in python 3 pass # ---------Debug logger--------- LOGGER = logging.getLogger() # ---------End of debug logger--------- def print_handler(msg, stderr=False): """Helper function for handling warning messages appropriately. If LOGGER level is set to 40 print out the warnings, else log them as a warning. :param msg: The warning message. :type msg: str """ # if override: if not stderr: sys.stdout.write(msg) else: sys.stderr.write(msg) # else: # if LOGGER.getEffectiveLevel() == 40: # LOGGER.warning(msg) def warning_handler(msg, override=False): """Helper function for handling warning messages appropriately. If LOGGER level is set to 40 print out the warnings, else log them as a warning. :param msg: The warning message. :type msg: str """ # if override: # sys.stdout.write(msg) # else: # if LOGGER.getEffectiveLevel() == 40: LOGGER.warning(msg) def validate_headers(instance, verbose=False): """Validates an instance is patchable. :param instance: Instance of the property to check. :type instance: :class:`redfish.ris.RisMonolithMemberv100` :param verbose: Flag to print or log more information. :type verbose: bool :returns: True if the setting is not patchable, False if it is. """ skip = False try: headervals = instance.resp.getheaders() for kii, val in headervals.items(): if kii.lower() == "allow": if "PATCH" not in val: if verbose: warning_handler("Skipping read-only path: %s\n" % instance.resp.request.path) skip = True except: pass return skip def merge_dict(currdict, newdict): """Merges dictionaries together. :param currdict: Dictionary that will absorb the second. :type currdict: dict :param newdict: Dictionary to merge into the first. :type newdict: dict """ for k, itemv2 in list(newdict.items()): itemv1 = currdict.get(k) if isinstance(itemv1, Mapping) and isinstance(itemv2, Mapping): merge_dict(itemv1, itemv2) elif isinstance(itemv1, list) and isinstance(itemv2, list): j = 0 for i in itemv2: if isinstance(i, dict): for k, v in i.items(): if k not in [d.keys() for d in itemv1]: itemv1[j][k] = v j = j + 1 else: itemv1.append(i) else: currdict[k] = itemv2 def get_errmsg_type(results): """Return the registry type of a response. :param results: rest response. :type results: :class:`redfish.rest.containers.RestResponse` :returns: A Registry Id type string, None if not match is found, or no_id if the response is not an error message :rtype: None or string """ message_type = None try: jsonpath_expr = jsonpath_rw.parse("$..MessageId") messageid = [match.value for match in jsonpath_expr.find(results.dict)] if not messageid: jsonpath_expr = jsonpath_rw.parse("$..MessageID") messageid = [match.value for match in jsonpath_expr.find(results.dict)] if messageid: message_type = messageid[0].split(".")[0] except: pass return message_type def filter_output(output, sel, val): """Filters a list of dictionaries based on a key:value pair only returning the dictionaries that include the key and value. :param output: List of dictionaries to check for the key:value. :type output: list :param sel: the key for the property to be filtered by. :type sel: str :param val: value for the property be filtered by. :type val: str :returns: A filtered list from output parameter :rtype: list """ # TODO: check if this can be replaced by navigatejson newoutput = [] if isinstance(output, list): for entry in output: if isinstance(entry, dict): if "/" in sel: sellist = sel.split("/") newentry = copy.copy(entry) for item in sellist: if item in list(newentry.keys()): if item == sellist[-1] and str(newentry[item]) == str(val): newoutput.append(entry) else: newentry = newentry[item] else: if sel in list(entry.keys()) and entry[sel] == val: newoutput.append(entry) else: return output return newoutput def checkallowablevalues(newdict=None, oridict=None): """Validate dictionary changes with Redfish allowable values. This will raise an :class:`redfish.ris.rmc_helper.IncorrectPropValue` error if the dictionary is not valid. :param newdict: dictionary with only the properties that have changed. :type newdict: dict :param oridict: Full dictionary with current state. (Includes @Redfish.AllowableValues) :type oridict: dict """ for strmatch in re.finditer("@Redfish.AllowableValues", str(oridict)): propname = str(oridict)[: strmatch.start()].split("'")[-1] strtomatch = "$..'{0}@Redfish.AllowableValues'".format(propname) jsonpath_expr = jsonpath_rw.parse(strtomatch) matches = jsonpath_expr.find(oridict) if matches: for match in matches: fullpath = str(match.full_path) if "Actions" in fullpath: continue checkpath = fullpath.split("@Redfish.AllowableValues")[0] jexpr2 = jsonpath_rw.parse(checkpath) valmatches = jexpr2.find(newdict) if valmatches: for mat in valmatches: res = [val for val in match.value if mat.value.lower() == val.lower()] if not res: raise IncorrectPropValue( "Incorrect Value " "entered. Please enter one of the below " "values for {0}:\n{1}".format("/".join(checkpath.split(".")), str(match.value)[1:-1]) ) def navigatejson(selector, currdict, val=None): """Function for navigating the json dictionary. Searches a dictionary for specific keys and possibly values, returning only the dictionary sections for the requested keys and values. :param selector: the property required from current dictionary. :type selector: list :param val: value to be filtered by. :type val: str or int or bool :param currdict: json dictionary of list to be filtered :type currdict: json dictionary/list :returns: returns a dictionary of selected items """ # TODO: Check for val of different types(bool, int, etc) temp_dict = dict() createdict = lambda y, x: {x: y} getkey = lambda cdict, sel: next((item for item in iterkeys(cdict) if sel.lower() == item.lower()), sel) getval = lambda cdict, sele: [cdict[sel] if sel in cdict else "~!@#$%^&*)()" for sel in [getkey(cdict, sele)]][0] fullbreak = False seldict = copy.deepcopy(currdict) for ind, sel in enumerate(selector): if isinstance(seldict, dict): selector[ind] = getkey(seldict, sel) seldict = getval(seldict, sel) if seldict == "~!@#$%^&*)()": return None if val and ind == len(selector) - 1: cval = ",".join(seldict) if isinstance(seldict, (list, tuple)) else seldict if not ( (val[-1] == "*" and str(cval).lower().startswith(val[:-1].lower())) or str(cval).lower() == val.lower() ): fullbreak = True elif isinstance(seldict, (list, tuple)): returndict = [] for items in seldict: correctcase = selector[ind:] returnseldict = navigatejson(correctcase, items) selector[ind:] = correctcase if returnseldict is not None: returndict.append(returnseldict) if returndict: seldict = returndict else: fullbreak = True if seldict: seldict = {selector[ind - 1]: seldict} selsdict = reduce(createdict, [seldict] + selector[: ind - 1][::-1]) merge_dict(temp_dict, selsdict) return temp_dict else: break else: fullbreak = True break if fullbreak: return None else: selsdict = reduce(createdict, [seldict] + selector[::-1]) merge_dict(temp_dict, selsdict) return temp_dict def iterateandclear(dictbody, proplist): """Iterate over a dictionary and remove listed properties. :param dictbody: json body :type dictbody: dict or list :param proplist: property list :type proplist: list """ if isinstance(dictbody, dict): _ = [dictbody.pop(key) for key in proplist if key in dictbody] for key in dictbody: dictbody[key] = iterateandclear(dictbody[key], proplist) if isinstance(dictbody, list): for ind, val in enumerate(dictbody): dictbody[ind] = iterateandclear(val, proplist) return dictbody def skipnonsettingsinst(instances): """Removes non /settings sections. Useful for only returning settings monolith members. Example: Members with paths `/redfish/v1/systems/1/bios/` and `/redfish/v1/systems/1/bios/settings` will return only the `/redfish/v1/systems/1/bios/settings` member. :param instances: list of :class:`redfish.ris.ris.RisMonolithMemberv100` instances to check for settings paths. :type instances: list :returns: list of :class:`redfish.ris.ris.RisMonolithMemberv100` setting instances :rtype: list """ instpaths = [inst.path.lower() for inst in instances] cond = list(filter(lambda x: x.endswith(("/settings", "settings/")), instpaths)) paths = [path.split("settings/")[0].split("/settings")[0] for path in cond] newinst = [inst for inst in instances if inst.path.lower() not in paths] return newinst def getattributeregistry(instances, adict=None): # add try except return {} after test """Gets an attribute registry in given monolith instances. :param instances: list of :class:`redfish.ris.ris.RisMonolithMemberv100` instances to be checked for attribute registry. :type instances: list :param adict: A dictionary containing an AttributeRegistry :type adict: dict :return: returns a dictionary containing the attribute registry string(s) :rtype: dict """ if adict: return adict.get("AttributeRegistry", None) newdict = {} for inst in instances: try: if "AttributeRegistry" in inst.resp.dict: if inst.defpath is not None: if not ("bios/settings" in inst.defpath): newdict[inst.maj_type] = inst.resp.obj["AttributeRegistry"] return newdict newdict[inst.maj_type] = inst.resp.obj["AttributeRegistry"] except AttributeError: LOGGER.warning("Invalid/Unpopulated Response: %s\nType:%s\nPath:%s\n" % (inst.resp, inst.type, inst.path)) return newdict def diffdict(newdict=None, oridict=None, settingskipped=[False]): """Diff two dictionaries, returning only the values that are different between the two. :param newdict: The first dictionary to check for differences. :type newdict: dict :param oridict: The second dictionary to check for differences. :type oridict: dict :param settingskipped: Flag to determine if any settings were missing. :type settingskipped: list :returns: dictionary with only the properties that have changed. :rtype: dict """ try: if newdict == oridict: return {} except: try: if set(newdict) == set(oridict): return {} except: pass newdictkeys = list(newdict.keys()) newdictlist = [] if type(oridict) is list: oridict = oridict[0] newdictlist.append(newdict) oridictkeys = list(oridict.keys()) newdictkeyslower = [ki.lower() for ki in newdictkeys] oridictkeyslower = [ki.lower() for ki in list(oridict.keys())] missingkeys = list(set(newdictkeyslower) - set(oridictkeyslower)) for kis in missingkeys: del newdict[newdictkeys[newdictkeyslower.index(kis)]] warning_handler("Attribute {0} not found in the selection...".format(kis)) settingskipped = [True] for key, val in list(newdict.items()): if key not in oridict: keycase = oridictkeys[oridictkeyslower.index(key.lower())] del newdict[key] key = keycase newdict[key] = val if isinstance(val, dict): res = diffdict(newdict[key], oridict[key]) if res: newdict[key] = res else: del newdict[key] elif isinstance(val, list): if val == oridict[key]: del newdict[key] continue if len(val) == 1 and isinstance(val[0], dict): if newdict[key] and oridict[key]: res = diffdict(newdict[key][0], oridict[key][0], settingskipped) if res: newdict[key][0] = res else: del newdict[key] if [li for li in val if not isinstance(li, string_types)]: continue else: if val: if [va.lower() for va in val] == [va.lower() if va else va for va in oridict[key]]: del newdict[key] # TODO: check if lowercase is correct or buggy for string types elif isinstance(val, (string_types, int, type(None))): if newdict[key] == oridict[key]: del newdict[key] if not newdictlist: return newdict else: return newdictlist def json_traversal(data, key_to_find, ret_dict=False): """ PENDING MODIFICATION TO MORE GENERALIZED NOTATION Recursive function to traverse a JSON resposne object and retrieve the array of relevant data (value or full key/value pair). Only a single key needs to be found within the dictionary in order to return a valid dictionary or value. #Intended Usage: - Error response message parsing - Checkreadunique in Validation :param data: json data to be parsed :type data: JSON error response object :param key_to_find: JSON key to be found :type key_to_find: String :param ret_dict: return dictionary instead of just value :type ret_dict: boolean :returns: value or dictionary containing 'key_to_find' (and all additional keys at the same level). """ try: for i, _iter in enumerate(data): try: if _iter == data: return None except Exception: pass try: if key_to_find.lower() == _iter.lower(): if ret_dict: return data else: return data[_iter] except Exception: pass try: if key_to_find.lower() in [str(_.lower()) for _ in _iter.keys()]: if ret_dict: return data else: return data[_iter] except Exception: pass _tmp = None try: if isinstance(data[_iter], dict): for k in data[_iter].keys(): if k.lower() == key_to_find.lower(): if ret_dict: return data[_iter] else: return data[_iter][k] _tmp = json_traversal(data[_iter], key_to_find, ret_dict) elif isinstance(data[_iter], list) or isinstance(data[_iter], tuple): try: _tmp = json_traversal(data[i], key_to_find, ret_dict) except Exception: _tmp = json_traversal(data[_iter], key_to_find, ret_dict) except Exception: _tmp = json_traversal(data[i], key_to_find, ret_dict) finally: if _tmp: return _tmp except Exception: pass def json_traversal_delete_empty(data, old_key=None, _iter=None, remove_list=None): """ Recursive function to traverse a dictionary and delete things which match elements in the remove_list :param data: to be truncated :type data: list or dict :param old_key: key from previous recursive call (higher in stack) :type old_key: dictionary key :param _iter: iterator tracker for list (tracks iteration across recursive calls) :type _iter: numerical iterator :param remove_list: list of items to be removed :type: list :returns: none """ if not remove_list: remove_list = ["NONE", None, "", {}, [], "::", "0.0.0.0", "Unknown"] list_quick_scan = False if isinstance(data, list): if _iter is None: for idx, val in enumerate(data): if idx is (len(data) - 1): list_quick_scan = True json_traversal_delete_empty(val, old_key, idx, remove_list) if list_quick_scan: for j in remove_list: for _ in range(data.count(j)): data.remove(j) elif isinstance(data, dict): delete_list = [] for key, value in data.items(): if ( (isinstance(value, dict) and len(value) < 1) or (isinstance(value, list) and len(value) < 1) or None or value in remove_list or key in remove_list ): delete_list.append(key) else: json_traversal_delete_empty(value, key, remove_list=remove_list) # would be great to not need this section; however, # since recursive deletion is not possible, this is needed # if you can figure out how to pass by reference then fix me! if (isinstance(value, dict) and len(value) < 1) or None or value in remove_list: delete_list.append(key) for dl_entry in delete_list: try: del data[dl_entry] except KeyError: pass