Files

1271 lines
49 KiB
Python

###
# 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 -*-
"""Monolith database implementation. Crawls Redfish and Legacy REST implementations
and holds all data retrieved. The created database is called the **monolith** and referenced as
such in the code documentation."""
# ---------Imports---------
import logging
import re
import sys
import threading
from re import error as regexerr
# Added for py3 compatibility
import six
try:
from collections import OrderedDict, defaultdict
except ImportError:
from collections.abc import OrderedDict, defaultdict
from queue import Queue
import jsonpath_rw
import jsonpointer
from jsonpointer import set_pointer
from six.moves.urllib.parse import urlparse, urlunparse
from six.moves.urllib.parse import ParseResult
from six.moves.urllib.parse import quote
from redfish.rest.containers import RestRequest, StaticRestResponse
from redfish.ris.ris_threaded import LoadWorker
from redfish.ris.sharedtypes import Dictable
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
# ---------End of debug logger---------
class BiosUnregisteredError(Exception):
"""Raised when BIOS has not been registered correctly in iLO"""
pass
class SchemaValidationError(Exception):
"""Schema Validation Class Error"""
pass
class SessionExpired(Exception):
"""Raised when session has expired"""
pass
class RisMonolithMemberBase(Dictable):
"""RIS monolith member base class"""
pass
class RisInstanceNotFoundError(Exception):
"""Raised when attempting to select an instance that does not exist"""
pass
class RisMonolithMemberv100(RisMonolithMemberBase):
"""Class used by :class:`RisMonolith` for holding information on a response and adds extra data
for monolith usage. A member can be marked as *modified* which means another operation may have
rendered this member out of date. It should be reloaded before continuing to ensure data is
up to date.
:param restresp: `RestResponse` to create a member from.
:type restresp: :class:`redfish.rest.containers.RestResponse`
:param isredfish: Flag if the response is redfish or not
:type isredfish: bool
"""
def __init__(self, restresp=None, isredfish=True):
self._resp = restresp
self._patches = list()
# Check if typedef can be used here
self._typestring = "@odata.type" if isredfish else "Type"
self.modified = False
self.defpath = self.deftype = self.defetag = self._type = None
self.__bool__ = self.__nonzero__
@property
def type(self):
"""Get type of the monolith member's response"""
try:
if self and self._typestring in self.resp.dict:
return self.resp.dict[self._typestring]
# Added for object type
elif self and "type" in self.resp.dict:
return self.resp.dict["type"]
except (AttributeError, ValueError, TypeError):
return self.deftype # data not yet fetched, probably empty dict, so assume deftype
return None
@property
def maj_type(self):
"""Get major type of the monolith member's response"""
if self.type:
if "." in self.type:
types = ".".join(self.type.split(".", 2)[:2])
retval = types[1:] if types.startswith("#") else types
else:
retval = self.type
return retval
return self.deftype
def __nonzero__(self):
"""Defining the bool value for the class"""
return True if self.resp else False
@property
def resp(self):
"""Get the entire response of the monolith member"""
return self._resp
@property
def path(self):
"""Get path of the monolith member's response"""
try:
if self:
return self.resp.request.path
except (AttributeError, ValueError):
pass
return self.defpath
@property
def patches(self):
"""Get patches for the monolith member"""
return self._patches
@patches.setter
def patches(self, val):
"""Set patches for the monolith member"""
self._patches = val
@property
def dict(self):
"""Get the dictionary of the monolith member's response"""
return self._resp.dict
@property
def etag(self):
"""Get the etag of the response"""
return self.defetag if not self.resp else self.resp.getheader("etag")
def popdefs(self, typename, pathval, etagval):
"""Populate the default values in the class
:param typename: The default **Type** string. Example: @odata.type
:type typename: str
:param pathval: The default **Path** string. Example: @odata.id
:type pathval: str
:param etagval: The default **ETag** value.
:type etagval: str
"""
self.defetag = etagval
self.deftype = typename
self.defpath = pathval
def to_dict(self):
"""Converts Monolith Member to a dictionary. This is the reverse of :func:`load_from_dict`.
:returns: returns the Monolith Member in dictionary form
"""
result = OrderedDict()
if self.maj_type:
result["Type"] = self.type
if self.resp:
if self.maj_type == "Collection.1" and "MemberType" in self.resp.dict:
result["MemberType"] = self.resp.dict["MemberType"]
result["links"] = OrderedDict()
result["links"]["href"] = ""
result["ETag"] = self.etag
if self.resp:
result["Content"] = self.resp.dict
result["Status"] = self.resp.status
result["Headers"] = self.resp.getheaders()
result["OriginalUri"] = self.path
result["Patches"] = self._patches
result["modified"] = self.modified
result["MajType"] = self.maj_type
return result
def load_from_dict(self, src):
"""Load variables to a monolith member from a dictionary.
This is the reverse of :func:`to_dict`.
:param src: Source to load member data from.
:type src: dict
"""
if "Type" in src:
self._type = src["Type"]
if "Content" in src:
restreq = RestRequest(method="GET", path=src["OriginalUri"])
src["restreq"] = restreq
self._resp = StaticRestResponse(**src)
self.deftype = src["MajType"]
self.defpath = src["OriginalUri"]
self.defetag = src["ETag"]
self._patches = src["Patches"]
self.modified = src["modified"]
class RisMonolith(Dictable):
"""Monolithic cache of RIS data. This takes a :class:`redfish.rest.v1.RestClient` and uses it to
gather data from a server and saves it in a modifiable database called monolith.
:param client: client to use for data retrieval.
:type client: :class:`redfish.rest.v1.RestClient`
:param typepath: The compatibility class to use for differentiating between Redfish/LegacyRest.
:type typepath: :class:`redfish.rest.ris.Typesandpathdefines`
:param directory_load: The flag to quick load using resource directory if possible.
When set to True this will load paths, etags, and types, but not create responses for
every monolith member. When responses are needed, they will need to be loaded separately.
:type directory_load: bool
"""
def __init__(self, client, typepath, directory_load=True):
self._client = client
self.name = "Monolithic output of RIS Service"
self._visited_urls = list()
self._type = None
self._name = None
self.progress = 0
self._directory_load = directory_load
self.is_redfish = self.client.is_redfish
self.typesadded = defaultdict(set)
self.paths = dict()
self.ctree = defaultdict(set)
self.colltypes = defaultdict(set)
self.typepath = typepath
self.collstr = self.typepath.defs.collectionstring
self.etagstr = "ETag"
if self.is_redfish:
self._resourcedir = "/redfish/v1/ResourceDirectory/"
else:
self._resourcedir = "/rest/v1/ResourceDirectory"
# MultiThreading
self.get_queue = Queue()
self.threads = []
@property
def directory_load(self):
"""The flag to gather information about a tree without downloading every path. Only usable
on HPE systems with a ResourceDirectory. type"""
return self._directory_load
@directory_load.setter
def directory_load(self, dir_load):
"""Set the directory_load flag"""
self._directory_load = dir_load
@property
def type(self):
"""Return monolith version type"""
return "Monolith.1.0.0"
@property
def visited_urls(self):
"""The urls visited by the monolith"""
return list(set(self._visited_urls) | set(self.paths.keys()))
@visited_urls.setter
def visited_urls(self, visited_urls):
"""Set visited URLS."""
self._visited_urls = visited_urls
@property
def types(self):
"""Returns list of types for members in the monolith
:rtype: list
"""
return list(self.typesadded.keys())
@types.setter
def types(self, member):
"""Adds a member to monolith
:param member: Member created based on response.
:type member: RisMonolithMemberv100
"""
self.typesadded[member.maj_type].add(member.path)
patches = []
if member.path in list(self.paths.keys()):
patches = self.paths[member.path].patches
self.paths[member.path] = member
self.paths[member.path].patches.extend([patch for patch in patches])
@property
def client(self):
"""Returns the current client object reference
:rtype: class object
"""
return self._client
@client.setter
def client(self, curr_client):
"""Set the current client
:param curr_client: current client object
:type curr_client: class object
"""
self._client = curr_client
def path(self, path):
"""Provides the member corresponding to the path specified. Case sensitive.
:param path: path of the monolith member to return
:type path: str
:rtype: RisMonolithMemberv100
"""
try:
return self.paths[path]
except:
return None
def iter(self, typeval=None):
"""An iterator that yields each member of monolith associated with a specific type. In the
case that no type is included this will yield all members in the monolith.
:rtype: RisMonolithMemberv100
"""
if not typeval:
for _, val in list(self.paths.items()):
yield val
else:
for typename in self.gettypename(typeval):
for item in self.typesadded[typename]:
yield self.paths[item]
# types = next(self.gettypename(typeval), None)
# if types in self.typesadded:
# for item in self.typesadded[types]:
# yield self.paths[item]
# else:
# raise RisInstanceNotFoundError("Unable to locate instance for" \
# " '%s'\n" % typeval)
def itertype(self, typeval):
"""Iterator that yields member(s) of given type in the monolith and raises an error if no
member of that type is found.
:param typeval: type name of the requested member.
:type typeval: str
:rtype: RisMonolithMemberv100
"""
typeiter = self.gettypename(typeval)
types = next(typeiter, None)
if types:
while types:
for item in self.typesadded[types]:
yield self.paths[item]
types = next(typeiter, None)
else:
raise RisInstanceNotFoundError("Unable to locate instance for '%s'\n" % typeval)
def typecheck(self, types):
"""Check if a member of given type exists in the monolith
:param types: type to check.
:type types: str
:rtype: bool
"""
if any(types in val for val in self.types):
return True
return False
def gettypename(self, types):
"""Takes a full type response and returns all major types associated.
Example: #Type.v1_0_0.Type will return iter(Type.1)
:param types: The type of the requested response.
:type types: str
:rtype: iter of major types
"""
types = types[1:] if types[0] in ("#", "#") else types
return iter((xt for xt in self.types if xt and types.lower() in xt.lower()))
def update_member(self, member=None, resp=None, path=None, init=True):
"""Adds member to the monolith. If the member already exists the
data is updated in place. Takes either a RisMonolithMemberv100 instance or a
:class:`redfish.rest.containers.RestResponse` along with that responses path.
:param member: The monolith member to add to the monolith.
:type member: RisMonolithMemberv100
:param resp: The rest response to add to the monolith.
:type resp: :class:`redfish.rest.containers.RestResponse`
:param path: The path correlating to the response.
:type path: str
:param init: Flag if addition is part of the initial load. Set this to false if you are
calling this by itself.
:type init: bool
"""
if not member and resp and path:
self._visited_urls.append(path.lower())
member = RisMonolithMemberv100(resp, self.is_redfish)
if not member: # Assuming for lack of member and not member.type
return
if not member.type:
member.deftype = "object" # Hack for general schema with no type
self.types = member
if init:
self.progress += 1
if LOGGER.getEffectiveLevel() == 40:
self._update_progress()
def load(
self,
path=None,
includelogs=False,
init=False,
crawl=True,
loadtype="href",
loadcomplete=False,
path_refresh=False,
json_out=False,
single=True,
):
"""Walks the entire data model and caches all responses or loads an individual path into
the monolith. Supports both threaded and sequential crawling.
:param path: The path to start the crawl from the provided path if crawling or
loads the path into monolith. If path is not included, crawl will start with
the default. The default is */redfish/v1/* or */rest/v1* depending on if the
system is Redfish or LegacyRest.
:type path: str.
:param includelogs: Flag to determine if logs should be downloaded as well in the crawl.
:type includelogs: bool
:param init: Flag to determine if this is the initial load.
:type init: bool
:param crawl: Flag to determine if load should crawl through found links.
:type crawl: bool
:param loadtype: Flag to determine if loading standard links: *href* or schema links: *ref*.
:type loadtype: str.
:param loadcomplete: Flag to download the entire data model including registries and
schemas.
:type loadcomplete: bool
:param path_refresh: Flag to reload the path specified, clearing any patches and overwriting the
current data in the monolith.
:type path_refresh: bool
"""
if init:
if LOGGER.getEffectiveLevel() >= 20 and not json_out:
sys.stdout.write("Discovering data...")
else:
LOGGER.info("Discovering data...")
self.name = self.name + " at %s" % self.client.base_url
selectivepath = path
if not selectivepath:
selectivepath = self.client.default_prefix
if loadtype == "href" and not self.client.base_url.startswith("blobstore://.") and not single:
if not self.threads:
for _ in range(6):
workhand = LoadWorker(self.get_queue)
workhand.setDaemon(True)
workhand.start()
self.threads.append(workhand)
self.get_queue.put(
(
selectivepath,
includelogs,
loadcomplete,
crawl,
path_refresh,
init,
None,
None,
self,
)
)
self.get_queue.join()
# Raise any errors from threads, and set them back to None after
excp = None
for thread in self.threads:
if excp is None:
excp = thread.get_exception()
thread.exception = None
if excp:
raise excp
# self.member_queue.join()
else:
# We can't load ref or local client in a threaded manner
self._load(
selectivepath,
originaluri=None,
crawl=crawl,
includelogs=includelogs,
init=init,
loadtype=loadtype,
loadcomplete=loadcomplete,
path_refresh=path_refresh,
prevpath=None,
)
if init:
if LOGGER.getEffectiveLevel() >= 20 and not json_out:
sys.stdout.write("Done\n")
else:
LOGGER.info("Done\n")
if self.directory_load and init:
self._populatecollections()
def loadallconfig(
self,
path=None,
includelogs=False,
init=False,
crawl=True,
loadtype="href",
loadcomplete=False,
path_refresh=False,
json_out=False,
single=True,
):
"""Walks the entire data model and caches all responses or loads an individual path into
the monolith. Supports both threaded and sequential crawling.
:param path: The path to start the crawl from the provided path if crawling or
loads the path into monolith. If path is not included, crawl will start with
the default. The default is */redfish/v1/* or */rest/v1* depending on if the
system is Redfish or LegacyRest.
:type path: str.
:param includelogs: Flag to determine if logs should be downloaded as well in the crawl.
:type includelogs: bool
:param init: Flag to determine if this is the initial load.
:type init: bool
:param crawl: Flag to determine if load should crawl through found links.
:type crawl: bool
:param loadtype: Flag to determine if loading standard links: *href* or schema links: *ref*.
:type loadtype: str.
:param loadcomplete: Flag to download the entire data model including registries and
schemas.
:type loadcomplete: bool
:param path_refresh: Flag to reload the path specified, clearing any patches and overwriting the
current data in the monolith.
:type path_refresh: bool
"""
selectivepath = path
if not selectivepath:
selectivepath = self.client.default_prefix
self._load(
selectivepath,
originaluri=None,
crawl=crawl,
includelogs=includelogs,
init=init,
loadtype=loadtype,
loadcomplete=loadcomplete,
path_refresh=path_refresh,
prevpath=None,
)
if init:
if LOGGER.getEffectiveLevel() >= 20 and not json_out:
sys.stdout.write("Done\n")
else:
LOGGER.info("Done\n")
if self.directory_load and init:
self._populatecollections()
def _load(
self,
path,
crawl=True,
originaluri=None,
includelogs=False,
init=True,
loadtype="href",
loadcomplete=False,
path_refresh=False,
prevpath=None,
):
"""Sequential version of loading monolith and parsing schemas.
:param path: path to start load from.
:type path: str
:param crawl: flag to determine if load should traverse found links.
:type crawl: bool
:param originaluri: variable to assist in determining originating path.
:type originaluri: str
:param includelogs: flag to determine if logs should be downloaded also.
:type includelogs: bool
:param init: flag to determine if first run of load.
:type init: bool
:param loadtype: flag to determine if load is meant for only href items.
:type loadtype: str.
:param loadcomplete: flag to download the entire monolith
:type loadcomplete: bool
:param path_refresh: flag to reload the members in the monolith instead of skip if they exist.
:type path_refresh: bool
"""
if (path.endswith("?page=1") or path.endswith(".json")) and not loadcomplete:
# Don't download schemas in crawl unless we are loading absolutely everything
return
elif path == "/" or "telemetry" in path.lower() or not path.startswith("/redfish/v1"):
return
elif not includelogs:
# Only include logs when asked as there can be an extreme amount of entries
if "/log" in path.lower():
return
# remove fragments and quote characters in path
p = urlparse(path)
p = ParseResult(
scheme=p.scheme, netloc=p.netloc, path=quote(p.path), params=p.params, query=p.query, fragment=""
)
path = urlunparse(p)
if prevpath and prevpath != path:
self.ctree[prevpath].update([path])
if not path_refresh:
if path.lower() in self.visited_urls:
return
LOGGER.debug("_loading %s", path)
resp = self.client.get(path)
if resp.status != 200 and path.lower() == self.typepath.defs.biospath:
raise BiosUnregisteredError()
elif resp.status == 401:
raise SessionExpired("Invalid session. Please logout and " "log back in or include credentials.")
elif resp.status not in (201, 200):
self.removepath(path)
return
if loadtype == "ref":
try:
if resp.status in (201, 200):
self.update_member(resp=resp, path=path, init=init)
self._parse_schema(resp)
except jsonpointer.JsonPointerException:
raise SchemaValidationError()
self.update_member(resp=resp, path=path, init=init)
fpath = lambda pa, path: (
path if pa.endswith(self.typepath.defs.hrefstring) and pa.startswith((self.collstr, "Entries")) else None
)
if loadtype == "href":
# follow all the href attributes
if self.is_redfish:
jsonpath_expr = jsonpath_rw.parse("$..'@odata.id'")
else:
jsonpath_expr = jsonpath_rw.parse("$..href")
matches = jsonpath_expr.find(resp.dict)
if "links" in resp.dict and "NextPage" in resp.dict["links"]:
if originaluri:
next_link_uri = originaluri + "?page=" + str(resp.dict["links"]["NextPage"]["page"])
href = "%s" % next_link_uri
self._load(
href,
originaluri=originaluri,
includelogs=includelogs,
crawl=crawl,
init=init,
prevpath=None,
loadcomplete=loadcomplete,
)
else:
next_link_uri = path + "?page=" + str(resp.dict["links"]["NextPage"]["page"])
href = "%s" % next_link_uri
self._load(
href,
originaluri=path,
includelogs=includelogs,
crawl=crawl,
init=init,
prevpath=None,
loadcomplete=loadcomplete,
)
# Only use monolith if we are set to
matchrdirpath = (
next((match for match in matches if match.value == self._resourcedir), None)
if self.directory_load
else None
)
if not matchrdirpath and crawl:
for match in matches:
if path == "/rest/v1" and not loadcomplete:
if (
str(match.full_path) == "links.Schemas.href"
or str(match.full_path) == "links.Registries.href"
):
continue
elif not loadcomplete:
if (
str(match.full_path) == "Registries.@odata.id"
or str(match.full_path) == "JsonSchemas.@odata.id"
):
continue
if match.value == path:
continue
elif not isinstance(match.value, six.string_types):
continue
href = "%s" % match.value
self._load(
href,
crawl=crawl,
originaluri=originaluri,
includelogs=includelogs,
init=init,
prevpath=fpath(str(match.full_path), path),
loadcomplete=loadcomplete,
)
elif crawl:
href = "%s" % matchrdirpath.value
self._load(
href,
crawl=crawl,
originaluri=originaluri,
includelogs=includelogs,
init=init,
prevpath=path,
loadcomplete=loadcomplete,
)
if loadcomplete:
if path == "/rest/v1":
schemamatch = jsonpath_rw.parse("$..extref")
else:
schemamatch = jsonpath_rw.parse("$..Uri")
smatches = schemamatch.find(resp.dict)
matches = matches + smatches
for match in matches:
if isinstance(match.value, six.string_types):
self._load(
match.value,
crawl=crawl,
originaluri=originaluri,
includelogs=includelogs,
init=init,
loadcomplete=loadcomplete,
prevpath=fpath(str(match.full_path), path),
)
def _parse_schema(self, resp):
"""Function to get and replace schema $ref with data
:param resp: response data containing ref items.
:type resp: str
"""
# pylint: disable=maybe-no-member
if not self.typepath.gencompany:
return self._parse_schema_gen(resp)
jsonpath_expr = jsonpath_rw.parse('$.."$ref"')
matches = jsonpath_expr.find(resp.dict)
respcopy = resp.dict
typeregex = r"([#,@].*?\.)"
if matches:
for match in matches:
fullpath = str(match.full_path)
jsonfile = match.value.split("#")[0]
jsonpath = match.value.split("#")[1]
listmatch = None
found = None
if "redfish.dmtf.org" in jsonfile:
if "odata" in jsonfile:
jsonpath = jsonpath.replace(jsonpath.split("/")[-1], "odata" + jsonpath.split("/")[-1])
jsonfile = "Resource.json"
found = re.search(typeregex, fullpath)
if found:
repitem = fullpath[found.regs[0][0] : found.regs[0][1]]
schemapath = "/" + fullpath.replace(repitem, "~").replace(".", "/").replace("~", repitem)
else:
schemapath = "/" + fullpath.replace(".", "/")
if ".json" in jsonfile:
itempath = schemapath
if self.is_redfish:
if resp.request.path[-1] == "/":
newpath = "/".join(resp.request.path.split("/")[:-2]) + "/" + jsonfile + "/"
else:
newpath = "/".join(resp.request.path.split("/")[:-1]) + "/" + jsonfile + "/"
else:
newpath = "/".join(resp.request.path.split("/")[:-1]) + "/" + jsonfile
if "href.json" in newpath:
continue
if newpath.lower() not in self.visited_urls:
self.load(
newpath,
crawl=False,
includelogs=False,
init=False,
loadtype="ref",
)
instance = list()
# deprecated type "string" for Type.json
if "string" in self.types:
for item in self.iter("string"):
instance.append(item)
if "object" in self.types:
for item in self.iter("object"):
instance.append(item)
for item in instance:
if jsonfile in item.path:
if "anyOf" in fullpath:
break
dictcopy = item.dict
try:
# TODO may need to really verify this is acceptable regex
listmatch = re.search("[][0-9]+[]", itempath)
except regexerr:
pass
# LOGGER.info("An error occurred with regex match on path: %s\n%s\n"\
# % (itempath, str(excp)))
if listmatch:
start = listmatch.regs[0][0]
end = listmatch.regs[0][1]
newitempath = [itempath[:start], itempath[end:]]
start = jsonpointer.JsonPointer(newitempath[0])
end = jsonpointer.JsonPointer(newitempath[1])
del start.parts[-1], end.parts[-1]
vals = start.resolve(respcopy)
count = 0
for val in vals:
try:
if "$ref" in six.iterkeys(end.resolve(val)):
end.resolve(val).pop("$ref")
end.resolve(val).update(dictcopy)
replace_pointer = jsonpointer.JsonPointer(end.path + jsonpath)
data = replace_pointer.resolve(val)
set_pointer(val, end.path, data)
start.resolve(respcopy)[count].update(val)
break
except:
count += 1
else:
itempath = jsonpointer.JsonPointer(itempath)
del itempath.parts[-1]
try:
if "$ref" in six.iterkeys(itempath.resolve(respcopy)):
itempath.resolve(respcopy).pop("$ref")
itempath.resolve(respcopy).update(dictcopy)
break
except jsonpointer.JsonPointerException:
pass
if jsonpath:
if "anyOf" in fullpath:
continue
if not jsonfile:
replacepath = jsonpointer.JsonPointer(jsonpath)
schemapath = schemapath.replace("/$ref", "")
if re.search(r"\[\d]", schemapath):
schemapath = schemapath.translate(str.maketrans("", "", "[]"))
schemapath = jsonpointer.JsonPointer(schemapath)
data = replacepath.resolve(respcopy)
if "$ref" in schemapath.resolve(respcopy):
schemapath.resolve(respcopy).pop("$ref")
schemapath.resolve(respcopy).update(data)
else:
if not listmatch:
schemapath = schemapath.replace("/$ref", "")
replacepath = schemapath + jsonpath
replace_pointer = jsonpointer.JsonPointer(replacepath)
try:
data = replace_pointer.resolve(respcopy)
set_pointer(respcopy, schemapath, data)
except jsonpointer.JsonPointerException:
# TODO
pass
resp.loaddict(respcopy)
else:
resp.loaddict(respcopy)
def _parse_schema_gen(self, resp):
"""Redfish general function to get and replace schema $ref with data
:param resp: response data containing ref items.
:type resp: str
"""
# pylint: disable=maybe-no-member
getval = lambda inmat: getval(inmat.left) + "/" + str(inmat.right) if hasattr(inmat, "left") else str(inmat)
respcopy = resp.dict
jsonpath_expr = jsonpath_rw.parse('$.."anyOf"')
while True:
matches = jsonpath_expr.find(respcopy)
if not matches:
break
match = matches[0]
newval = None
schlist = match.value
schlist = [ele for ele in list(schlist) if ele != {"type": "null"}]
norefsch = [ele for ele in list(schlist) if isinstance(ele, dict) and len(ele.keys()) > 1]
if norefsch:
newval = norefsch[0]
else:
newsc = [ele for ele in list(schlist) if not ele["$ref"].split("#")[0]]
newval = newsc[0] if newsc else None
if not newval:
schlist = [
ele["$ref"]
for ele in list(schlist)
if "$ref" in ele.keys()
and (ele["$ref"].split("#")[0].endswith(".json") and "odata" not in ele["$ref"].split("#")[0])
]
maxsch = max(schlist)
newval = {"$ref": maxsch}
itempath = "/" + getval(match.full_path)
if re.search(r"\[\d+]", itempath):
itempath = itempath.translate(str.maketrans("", "", "[]"))
itempath = jsonpointer.JsonPointer(itempath)
del itempath.parts[-1]
if "anyOf" in six.iterkeys(itempath.resolve(respcopy)):
itempath.resolve(respcopy).pop("anyOf")
itempath.resolve(respcopy).update(newval)
jsonpath_expr = jsonpath_rw.parse('$.."$ref"')
matches = jsonpath_expr.find(respcopy)
if matches:
for _, match in enumerate(matches):
jsonfile = match.value.split("#")[0]
jsonfile = "" if jsonfile.lower() == resp.request.path.lower() else jsonfile
jsonpath = match.value.split("#")[1]
schemapath = "/" + getval(match.full_path)
if jsonfile:
itempath = schemapath
if "/" not in jsonfile:
inds = -2 if resp.request.path[-1] == "/" else -1
jsonfile = "/".join(resp.request.path.split("/")[:inds]) + "/" + jsonfile + "/"
if jsonfile not in self.paths:
self.load(
jsonfile,
crawl=False,
includelogs=False,
init=False,
loadtype="ref",
)
item = self.paths[jsonfile] if jsonfile in self.paths else None
if not item:
if "anyOf" not in schemapath:
raise SchemaValidationError()
continue
if re.search(r"\[\d+]", itempath):
itempath = itempath.translate(str.maketrans("", "", "[]"))
itempath = jsonpointer.JsonPointer(itempath)
del itempath.parts[-1]
try:
if "$ref" in six.iterkeys(itempath.resolve(respcopy)):
itempath.resolve(respcopy).pop("$ref")
itempath.resolve(respcopy).update(item.dict)
except jsonpointer.JsonPointerException:
pass
if jsonpath:
schemapath = schemapath.replace("/$ref", "")
if re.search(r"\[\d+]", schemapath):
schemapath = schemapath.translate(str.maketrans("", "", "[]"))
if not jsonfile:
replacepath = jsonpointer.JsonPointer(jsonpath)
schemapath = jsonpointer.JsonPointer(schemapath)
data = replacepath.resolve(respcopy)
if "$ref" in schemapath.resolve(respcopy):
schemapath.resolve(respcopy).pop("$ref")
schemapath.resolve(respcopy).update(data)
else:
replacepath = schemapath + jsonpath
replace_pointer = jsonpointer.JsonPointer(replacepath)
data = replace_pointer.resolve(respcopy)
set_pointer(respcopy, schemapath, data)
resp.loaddict(respcopy)
else:
resp.loaddict(respcopy)
def load_from_dict(self, src):
"""Load data to monolith from a dict. This is the reverse of :func:`to_dict`.
:param src: data receive from rest operation.
:type src: str
"""
self._type = src["Type"]
self._name = src["Name"]
self.typesadded = defaultdict(set, {ki: set(val) for ki, val in src["typepath"].items()})
self.ctree = defaultdict(set, {ki: set(val) for ki, val in src["ctree"].items()})
self.colltypes = defaultdict(set, {ki: set(val) for ki, val in src["colls"].items()})
for _, resp in list(src["resps"].items()):
member = RisMonolithMemberv100(None, self.is_redfish)
member.load_from_dict(resp)
self.update_member(member=member, init=False)
def to_dict(self):
"""Convert data to a dict from monolith. This is the reverse of :func:`load_from_dict`."""
result = OrderedDict()
result["Type"] = self.type
result["Name"] = self.name
result["typepath"] = self.typesadded
result["ctree"] = self.ctree
result["colls"] = self.colltypes
result["resps"] = {x: v.to_dict() for x, v in list(self.paths.items())}
return result
def markmodified(self, opath, path=None, modpaths=None):
"""Mark the paths to be modifed which are connected to current path. When calling this
function you only need to include `opath`.
:param opath: original path which has been modified
:type opath: str
"""
modpaths = set() if modpaths is None else modpaths
path = path if path else opath
if not path:
return
modpaths.update(self.ctree[path] if path in self.ctree else set())
self.paths[path].modified = True
for npath in [
unmodpath for unmodpath in modpaths if unmodpath in self.paths and not self.paths[unmodpath].modified
]:
self.markmodified(opath, path=npath, modpaths=modpaths)
return modpaths
def checkmodified(self, opath, path=None, modpaths=None):
"""Check if the path or its children are modified. When calling this
function you only need to include `opath`.
:param opath: original path which has been modified
:type opath: str
"""
# return [paths for paths in self.ctree[path] if self.paths[paths].modified]
modpaths = set() if modpaths is None else modpaths
path = path if path else opath
newpaths = set()
if not path:
return
if path in self.paths and self.paths[path].modified:
newpaths = (
set([conn for conn in self.ctree[path] if conn in self.paths and self.paths[path].modified]) - modpaths
)
modpaths.update(newpaths | set([path]))
for npath in [unmodpath for unmodpath in newpaths]:
self.checkmodified(opath, path=npath, modpaths=modpaths)
return modpaths
def removepath(self, path):
"""Remove a given path from the cache
:param path: path which is to be checked if modified
:type path: str
"""
if path in self._visited_urls:
self._visited_urls.remove(path)
if path not in self.paths:
return
if path in self.typesadded[self.paths[path].maj_type]:
self.typesadded[self.paths[path].maj_type].remove(path)
if not self.typesadded[self.paths[path].maj_type]:
del self.typesadded[self.paths[path].maj_type]
del self.paths[path]
if path in self.ctree:
del self.ctree[path]
_ = [self.ctree[paths].remove(path) for paths in self.ctree if path in self.ctree[paths]]
def _populatecollections(self):
"""Populate the collections type and types depending on resourcedirectory"""
if self._resourcedir not in self.paths:
return
self.colltypes = defaultdict(set)
alltypes = []
colls = []
for item in self.paths[self._resourcedir].dict["Instances"]:
# Fix for incorrect RDir instances.
if self.typepath.defs.typestring not in item or item[self.typepath.defs.hrefstring] in self.paths:
continue
typename = ".".join(item[self.typepath.defs.typestring].split(".", 2)[:2]).split("#")[-1]
_ = [alltypes.append(typename) if "Collection" not in typename else None]
_ = [colls.append(typename) if "Collection" in typename else None]
member = RisMonolithMemberv100(None, self.is_redfish)
member.popdefs(typename, item[self.typepath.defs.hrefstring], item[self.etagstr])
self.update_member(member=member, init=False)
for coll in colls:
collname = coll.split("Collection")[0].split("#")[-1]
typename = next((name for name in alltypes if name.startswith(collname)), None)
colltype = ".".join(coll.split(".", 2)[:2]).split("#")[-1]
self.colltypes[typename].add(colltype)
def capture(self, redmono=False, single=True):
"""Crawls the server and returns the monolith data or just headers and responses.
:param redmono: If True, returns only headers and responses; otherwise, returns full monolith data.
:type redmono: bool
:rtype: dict
"""
self.load(includelogs=False, crawl=True, loadcomplete=True, path_refresh=True, init=True, single=single)
if not redmono:
return self.to_dict()
ret = {}
try:
for x, v in self.paths.items():
if v:
v_resp = v.resp
if v_resp:
ret[x] = {"Headers": v_resp.getheaders(), "Response": v_resp.dict}
except Exception as e:
LOGGER.debug("Error in capture: %s", e)
ret = {}
return ret
def normalize_urls(self, data):
"""
Normalize URL-like dictionary keys:
- If both '/path' and '/path/' exist, keep only '/path/'.
- If only one version exists, ensure the key ends with '/'.
- All final keys end with '/'.
"""
normalized_data = {}
seen = set()
for key in data:
# Normalize key with trailing slash
if key.endswith("/"):
base_key = key[:-1]
trailing_key = key
else:
base_key = key
trailing_key = key + "/"
# Avoid processing the same base path twice
if base_key in seen:
continue
seen.add(base_key)
# Prefer version with trailing slash
if trailing_key in data:
normalized_data[trailing_key] = data[trailing_key]
elif base_key in data:
normalized_data[trailing_key] = data[base_key]
return normalized_data
def fetch_data(self, path):
resp = self.client.get(path)
return resp
def captureallconfig(self, single=True, exclueurl=None):
"""Crawls the server and returns the monolith data or just headers and responses.
:param redmono: If True, returns only headers and responses; otherwise, returns full monolith data.
:type redmono: bool
:rtype: dict
"""
self.loadallconfig(
includelogs=False, crawl=True, loadcomplete=True, path_refresh=True, init=True, single=single
)
ret = {}
try:
for x, v in self.paths.items():
if exclueurl.lower() != x.lower():
if v:
if v.resp is not None:
ret[x] = {"Headers": v.resp.getheaders(), "Response": v.resp.dict}
else:
resp = self.fetch_data(x)
ret[x] = {"Headers": resp.getheaders(), "Response": resp.dict}
except Exception as e:
LOGGER.debug("Error in capture: %s", e)
ret = {}
ret = self.normalize_urls(ret)
return ret
def killthreads(self):
"""Function to kill threads on logout"""
threads = []
for thread in threading.enumerate():
if isinstance(thread, LoadWorker):
self.get_queue.put(
(
"KILL",
"KILL",
"KILL",
"KILL",
"KILL",
"KILL",
"KILL",
"KILL",
"KILL",
"KILL",
)
)
threads.append(thread)
for thread in threads:
thread.join()
def _update_progress(self):
"""Simple function to increment the dot progress"""
if self.progress % 6 == 0:
sys.stdout.write(".")