iLO-manager first main commitgit status

This commit is contained in:
Jiří Stach committed 2026-02-03 15:56:53 +01:00
1 parent e6f4cc04ca
commit 599d8da562
3271 files changed
+550655

No files matched your search

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
"""
Expanded LegacyREST/Redfish interface for schema validation, database for responses, caching,
and error registries.
"""
from .ris import (
RisInstanceNotFoundError,
RisMonolith,
RisMonolithMemberBase,
RisMonolithMemberv100,
SessionExpired,
)
from .rmc import RmcApp
from .rmc_helper import (
CurrentlyLoggedInError,
IdTokenError,
IloLicenseError,
InstanceNotFoundError,
InvalidSelectionError,
NothingSelectedError,
NothingSelectedFilterError,
NothingSelectedSetError,
RmcCacheManager,
RmcFileCacheManager,
ScepenabledError,
UndefinedClientError,
ValidationError,
ValueChangedError,
)
from .sharedtypes import JSONEncoder
from .validation import RegistryValidationError, ValidationManager
@@ -0,0 +1,459 @@
###
# 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 -*-
"""Compatibility functionality in between iLO versions and generic Redfish/LegacyRest servers.
Used to provide convenient string variables that are usable on any iLO irrespective of version or
API type."""
# ---------Imports---------
import logging
from redfish import LegacyRestClient, RedfishClient
from redfish.rest.v1 import ServerDownOrUnreachableError
from redfish.ris.rmc_helper import (
NothingSelectedError,
UnableToObtainIloVersionError,
UserNotAdminError,
)
# ---------End of imports---------
LOGGER = logging.getLogger(__name__)
# TODO: This will be a part of the compatability class
class Typesandpathdefines(object):
"""The global types and path definitions class. Holds information on a system and automatically
creates the correct type strings along with some generic paths. Paths are meant to be used with
iLO systems. Paths may be different on generic Redfish systems. Self variables are created when
the `getgen` function is called.
Useful self variables that are created include:
* **url**: The url of the system that the defines were created for.
* **defs**: The string defines for the system that was passed to `getgen`. Includes key
property keys, paths, types, and flags to check what the system type is.
* **ilogen**: The iLO generation of the system that the defines were created for. For non-iLO
Redfish systems this is set to **5**.
* **iloversion**: The iLO version of the system that the defines were created for.
* **flagiften**: Flag is set to true if the system is Gen 10 or a non-iLO Redfish system.
"""
def __init__(self):
self.url = None
self.defs = None
self.ilogen = None
self.iloversion = None
self.flagiften = False
self.adminpriv = True
def iLO7VersionCheck(self, rootresp):
if "Oem" in rootresp:
if "Hpe" in rootresp["Oem"]:
gen = next(iter(rootresp.get("Oem", {}).get("Hpe", {}).get("Manager", {}))).get("ManagerType", None)
elif "Hp" in rootresp["Oem"]:
gen = next(iter(rootresp.get("Oem", {}).get("Hp", {}).get("Manager", {}))).get("ManagerType", None)
else:
raise Exception("Malformed Json")
if gen:
gen = gen.split(" ")[1]
else:
gen = rootresp.get("Oem", {}).get("Hpe", {}).get("Moniker", {}).get("PRODGEN").split(" ")[1]
if int(gen) == 7:
return True
return False
def getgen(
self,
gen=None,
url=None,
username=None,
password=None,
sessionid=None,
logger=None,
proxy=None,
ca_cert_data={},
isredfish=True,
login_otp=None,
log_dir=None,
session_location=None,
):
"""Function designed to verify the servers platform. Will generate the `Typeandpathdefines`
variables based on the system type that is detected.
:param url: The URL to perform the request on.
:type url: str
:param username: The username to login with.
:type username: str
:param password: The password to login with.
:type password: str
:param proxy: The proxy to connect to the system with.
:type proxy: str
:param ca_certs: Dictionary including the TLS certificate information of user based
authentication
:type ca_certs: dict
:param isredfish: The flag to force redfish conformance on iLO 4 systems. You will still
need to call `updatedefinesflag` to update the defines to Redfish.
:type isredfish: bool
:param logger: The logger handler to log data too uses the default if none is provided.
:type logger: str
"""
if self.adminpriv is False and url.startswith("blob"):
raise UserNotAdminError("")
self.url = url
self.is_redfish = isredfish
self.gencompany = self.rootresp = False
self.ilogen = 5 # If no iLO or Anonymous data , default to iLO 5 types
logger = logger if not logger else LOGGER
client = None
self.noschemas = False
self.schemapath = self.regpath = ""
if not gen:
try_count = 0
try:
client = RedfishClient(
base_url=self.url,
username=username,
password=password,
sessionid=sessionid,
proxy=proxy,
ca_cert_data=ca_cert_data,
login_otp=login_otp,
log_dir=log_dir,
session_location=session_location,
)
client._get_root()
except ServerDownOrUnreachableError as excp:
if self.is_redfish:
raise excp
try_count += 1
if not self.is_redfish:
try:
restclient = LegacyRestClient(
base_url=self.url,
username=username,
password=password,
sessionid=sessionid,
proxy=proxy,
ca_cert_data=ca_cert_data,
login_otp=login_otp,
session_location=session_location,
)
restclient._get_root()
# Check that the response is actually legacy rest and not a redirect
_ = restclient.root.obj.Type
self.is_redfish = False
client = restclient
except Exception as excp:
try_count += 1
if not client:
logger.info("Gen get rest error:" + str(excp) + "\n")
raise excp
else:
self.is_redfish = True
if try_count > 1:
raise ServerDownOrUnreachableError("Server not reachable or does not support " "HPRest or Redfish \n")
rootresp = client.root.obj
self.rootresp = rootresp
client.logout()
iLO7 = False
iLO7 = self.iLO7VersionCheck(rootresp)
if not iLO7:
self.gencompany = next(iter(self.rootresp.get("Oem", {}).keys()), None) in (
"Hpe",
"Hp",
)
comp = "Hp" if self.gencompany else None
comp = "Hpe" if rootresp.get("Oem", {}).get("Hpe", None) else comp
if comp and next(iter(rootresp.get("Oem", {}).get(comp, {}).get("Manager", {}))).get(
"ManagerType", None
):
self.ilogen = next(iter(rootresp.get("Oem", {}).get(comp, {}).get("Manager", {}))).get(
"ManagerType"
)
self.ilover = next(iter(rootresp.get("Oem", {}).get(comp, {}).get("Manager", {}))).get(
"ManagerFirmwareVersion"
)
if self.ilogen.split(" ")[-1] == "CM":
# Assume iLO 4 types in Moonshot
self.ilogen = 4
self.iloversion = None
else:
self.iloversion = float(self.ilogen.split(" ")[-1] + "." + "".join(self.ilover.split(".")))
else:
self.ilogen = next(iter(rootresp.get("Oem", {}).get("Hpe", {}).get("Manager", {}))).get("ManagerType")
self.ilover = next(iter(rootresp.get("Oem", {}).get("Hpe", {}).get("Manager", {}))).get(
"ManagerFirmwareVersion", None
)
if self.ilover is None:
self.ilover = "1.01.01"
self.iloversion = float(self.ilogen.split(" ")[-1] + "." + "".join(self.ilover.split(".")))
else:
self.ilogen = int(gen)
try:
if not isinstance(self.ilogen, int):
self.ilogen = int(self.ilogen.split(" ")[-1])
self.flagiften = True if int(self.ilogen) >= 5 else False
except:
raise UnableToObtainIloVersionError("Unable to find the iLO generation.")
self.noschemas = (
True
if self.rootresp and "JsonSchemas" in self.rootresp and not self.rootresp.get("JsonSchemas", None)
else False
)
if self.noschemas:
self.ilogen = self.ilover = self.iloversion = None
if self.rootresp and not self.noschemas:
self.defineregschemapath(self.rootresp)
if self.flagiften:
self.defs = Definevalstenplus()
else:
self.defs = DefinevalsNine()
def defineregschemapath(self, rootobj):
"""Defines the schema and registry paths using data in root path.
:param rootobj: The root path data.
:type rootobj: dict.
"""
if self.iLO7VersionCheck(rootobj):
self.gencompany = "Hpe"
else:
self.gencompany = next(iter(rootobj.get("Oem", {}).keys()), None) in ("Hpe", "Hp")
self.schemapath = (
rootobj["JsonSchemas"]["@odata.id"]
if rootobj.get("JsonSchemas", None)
else rootobj["links"]["Schemas"]["href"]
)
self.schemapath = (
self.schemapath.rstrip("/") + "/?$expand=."
if self.is_redfish and self.flagiften and self.gencompany
else self.schemapath
)
self.regpath = (
rootobj["Registries"]["@odata.id"]
if rootobj.get("Registries", None)
else rootobj["links"]["Registries"]["href"]
)
self.regpath = (
self.regpath.rstrip("/") + "/?$expand=."
if self.is_redfish and self.flagiften and self.gencompany
else self.regpath
)
# TODO: Move these to a compatability class
def updatedefinesflag(self, redfishflag=None):
"""Updates the redfish and rest flag depending on system and redfishflag input. On an iLO 5
system or another Redfish system, this will do nothing. On an iLO 4 system with both Redfish
and LegacyRest this will update the defines to redfish if the *redfishflag* is set to True
and stay with the LegacyRest defines otherwise.
:param redfishflag: User input for redfish
:type redfishflag: bool
:returns: True if the system should use Redfish, False for legacy Rest.
:rtype: bool
"""
if self.defs:
is_redfish = redfishflag or self.defs.isgen10
self.defs.flagforrest = not is_redfish
if is_redfish:
self.defs.redfishchange()
return is_redfish
else:
return redfishflag
def modifyselectorforgen(self, sel):
"""Changes the select to match the Generation's HP string based to the correct type for
specific iLO versions.
:param sel: query to be changed to match Generation's HP string
:type sel: str
:returns: A modified selector matching the Generation's HP string.
:rtype: string
"""
if not sel:
raise NothingSelectedError()
sel = sel.lower()
returnval = sel
if sel.startswith(("hpeeskm", "#hpeeskm", "hpeskm", "#hpeskm")):
returnval = self.defs.hpeskmtype
elif "bios." in sel[:9].lower():
returnval = self.defs.biostype
elif sel.startswith(("hpe", "#hpe")) and self.defs and self.defs.isgen9:
returnval = sel[:4].replace("hpe", "hp") + sel[4:]
elif not sel.startswith(("hpe", "#hpe")) and self.defs and self.defs.isgen10:
returnval = sel[:3].replace("hp", "hpe") + sel[3:]
return returnval
class Definevals(object):
"""Base class for setting platform dependent variables."""
def __init__(self):
pass
class Definevalstenplus(Definevals):
"""Platform dependent variables for iLO 5+ (Gen 10)."""
# pylint: disable=too-many-instance-attributes
# As a defines class this will need all the attributes
def __init__(self):
self.oemhp = "Hpe"
self.oempath = "/Oem/Hpe"
self.startpath = "/redfish/v1/"
self.systempath = "/redfish/v1/Systems/1/"
self.managerpath = "/redfish/v1/Managers/1/"
self.biospath = "/redfish/v1/systems/1/bios/"
self.addlicensepath = "/redfish/v1/Managers/1/LicenseService/"
self.accountspath = "/redfish/v1/AccountService/Accounts/"
self.federationpath = "/redfish/v1/Managers/1/FederationGroups/"
self.resourcedirpath = "/redfish/v1/ResourceDirectory/"
self.biostype = "Bios."
self.hpeskmtype = "HpeESKM."
self.hpcommontype = "HpeCommon"
self.hpilossotype = "HpeiLOSSO."
self.hpsecureboot = "SecureBoot."
self.logservicetype = "#LogService."
self.iscsisource = "iSCSISources"
self.iscsiattemptinstance = "iSCSIAttemptInstance"
self.iscsiattemptname = "iSCSIAttemptName"
self.hphttpscerttype = "HpeHttpsCert."
self.snmpservice = "HpeiLOSnmpService."
self.attributenametype = "AttributeName"
self.hpilodatetimetype = "HpeiLODateTime."
self.attributeregtype = "#AttributeRegistry."
self.hpilofirmwareupdatetype = "UpdateService."
self.resourcedirectorytype = "HpeiLOResourceDirectory."
self.hpilofederationgrouptype = "HpeiLOFederationGroup."
self.managernetworkservicetype = "ManagerNetworkProtocol."
self.schemafilecollectiontype = "#JsonSchemaFileCollection."
self.regfilecollectiontype = "#MessageRegistryFileCollection."
self.hpilolicensecollectiontype = "HpeiLOLicenseCollection."
self.hpiloactivehealthsystemtype = "#HpeiLOActiveHealthSystem."
self.securityservice = "HpeSecurityService."
self.hpiscsisoftwareinitiatortype = "HpeiSCSISoftwareInitiator."
self.hpilofederationgrouptypecoll = "HpeiLOFederationGroupCollection."
self.bootoverridetargettype = "BootSourceOverrideTarget@Redfish.AllowableValues"
self.messageregistrytype = "#MessageRegistry."
self.typestring = "@odata.type"
self.hrefstring = "@odata.id"
self.collectionstring = "Members"
self.biossettingsstring = "@Redfish.Settings"
self.attname = "AttributeName"
self.iscsistring = "iSCSISources"
self.isgen9 = False
self.isgen10 = True
self.flagforrest = False
super(Definevalstenplus, self).__init__()
def redfishchange(self):
"""Empty function to update redfish variables (unneeded when the system is already redfish)."""
pass
class DefinevalsNine(Definevals):
"""Platform dependent variables for iLO 4 LegacyRest (Gen 9)."""
# pylint: disable=too-many-instance-attributes
# As a defines class this will need all the attributes
def __init__(self):
self.oemhp = "Hp"
self.oempath = "/Oem/Hp"
self.startpath = "/rest/v1"
self.systempath = "/rest/v1/Systems/1"
self.managerpath = "/rest/v1/Managers/1"
self.biospath = "/rest/v1/systems/1/bios"
self.addlicensepath = "/rest/v1/Managers/1/LicenseService"
self.accountspath = "/rest/v1/AccountService/Accounts"
self.federationpath = "/rest/v1/Managers/1/FederationGroups"
self.resourcedirpath = "/rest/v1/ResourceDirectory"
self.biostype = "HpBios."
self.hpeskmtype = "HpESKM."
self.hpcommontype = "HpCommon"
self.hpilossotype = "HpiLOSSO."
self.snmpservice = "SnmpService."
self.attributenametype = "Name"
self.logservicetype = "LogService."
self.iscsisource = "iSCSIBootSources"
self.iscsiattemptinstance = "iSCSIBootAttemptInstance"
self.iscsiattemptname = "iSCSIBootAttemptName"
self.hpsecureboot = "HpSecureBoot."
self.hphttpscerttype = "HpHttpsCert."
self.hpilodatetimetype = "HpiLODateTime."
self.hpilofirmwareupdatetype = "HpiLOFirmwareUpdate."
self.resourcedirectorytype = "HpiLOResourceDirectory."
self.hpilofederationgrouptype = "HpiLOFederationGroup."
self.attributeregtype = "HpBiosAttributeRegistrySchema."
self.schemafilecollectiontype = "#SchemaFileCollection."
self.regfilecollectiontype = "#SchemaFileCollection."
self.managernetworkservicetype = "ManagerNetworkService."
self.hpiloactivehealthsystemtype = "HpiLOActiveHealthSystem."
self.messageregistrytype = "MessageRegistry."
self.hpilolicensecollectiontype = None
self.hpilofederationgrouptypecoll = None
self.bootoverridetargettype = "BootSourceOverrideSupported"
self.hpiscsisoftwareinitiatortype = "HpiSCSISoftwareInitiator"
self.typestring = "Type"
self.hrefstring = "href"
self.collectionstring = "Items"
self.biossettingsstring = "SettingsResult"
self.attname = "Name"
self.iscsistring = "iSCSIBootSources"
self.isgen9 = True
self.isgen10 = False
self.flagforrest = True
super(DefinevalsNine, self).__init__()
def redfishchange(self):
"""Function to update redfish variables from LegacyRest to iLO 4 Redfish (Gen 9)."""
self.startpath = "/redfish/v1/"
self.systempath = "/redfish/v1/Systems/1/"
self.managerpath = "/redfish/v1/Managers/1/"
self.biospath = "/redfish/v1/systems/1/bios/"
self.addlicensepath = "/redfish/v1/Managers/1/LicenseService/"
self.resourcedirpath = "/redfish/v1/ResourceDirectory/"
self.typestring = "@odata.type"
self.hrefstring = "@odata.id"
self.collectionstring = "Members"
self.logservicetype = "#LogService."
self.hpiloactivehealthsystemtype = "#HpiLOActiveHealthSystem."
self.hpilolicensecollectiontype = "HpiLOLicenseCollection."
self.hpilofederationgrouptypecoll = "HpiLOFederationGroupCollection."
self.managernetworkservicetype = "ManagerNetworkProtocol."
self.flagforrest = False
@@ -0,0 +1,365 @@
###
# 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 -*-
"""Error response handler for Redfish or LegacryRest responses. Extended information only available
with registries available on system, otherwise will return generic error responses."""
import logging
from redfish.ris.ris import SessionExpired
from redfish.ris.rmc_helper import (
EmptyRaiseForEAFP,
IdTokenError,
IloLicenseError,
IloResponseError,
ScepenabledError,
ValueChangedError,
)
from redfish.ris.utils import (
get_errmsg_type,
json_traversal,
print_handler,
warning_handler,
)
# ---------Debug logger---------
LOGGER = logging.getLogger()
# ---------End of debug logger---------
class ResponseHandler(object):
"""Class to handle error responses from the server.
:param validation_mgr: ValidationManager instance to gather registries if needed. Available
in an RmcApp class as an attribute.
:type validation_mgr: ValidationManager
:param msg_reg_type: Redfish (#MessageRegistry.) or LegacyRest (MessageRegistry.)
message registry string. Available in Typesandpathdefines class.
:type msg_reg_type: str
"""
def __init__(self, validaition_mgr, msg_type):
self.validation_mgr = validaition_mgr
self.msg_reg_type = msg_type
def output_resp(self, response, dl_reg=False, verbosity=1, stderr_flag=False):
"""Prints or logs parsed MessageId response. Will raise an IloResponseError or return
a list of message response data which includes the information returned from
message_handler.
:param response: message response of a call.
:type response: :class:`redfish.rest.containers.RestResponse`
:param dl_reg: Flag to download registry. If this is set to True a generic message response
will be returned instead of gathered from registries.
:type dl_reg: bool
:param verbosity: Optional verbosity level. Only modifies what is output to log or screen.
:type verbosity: int
:returns: List of error response dictionaries.
"""
retdata = None
if response.status > 299:
message_text = "No error message returned or unable to parse error response."
else:
message_text = "The operation completed successfully."
if response.status < 300 and (response._rest_request.method == "GET" or not response.read):
# for rawget
if verbosity == 0:
verbosity = 1
print_handler(
self.verbosity_levels(
message=message_text, response_status=response.status, verbosity=verbosity, dl_reg=dl_reg
),
stderr_flag,
)
elif response.status == 401:
raise SessionExpired()
elif response.status == 403:
results = response.dict["error"]["@Message.ExtendedInfo"]
for result in results:
if "License" in list(result.values())[0] or "license" in list(result.values())[0]:
raise IloLicenseError("")
raise IdTokenError()
elif response.status == 412:
warning_handler(
"The property you are trying to change has been updated. "
"Please check entry again before manipulating it.\n",
override=True,
)
raise ValueChangedError()
else:
retdata = self.message_handler(
response_data=response,
verbosity=verbosity,
message_text=message_text,
dl_reg=dl_reg,
stderr_flag=stderr_flag,
)
if response.status == 400:
results = response.dict["error"]["@Message.ExtendedInfo"]
for result in results:
if "License" in list(result.values())[0] or "license" in list(result.values())[0]:
raise IloLicenseError("")
if "UnsupportedOperationACEEnabled" in list(result.values())[0]:
raise ScepenabledError("")
if response.status > 299:
raise IloResponseError("")
else:
return retdata
def message_handler(self, response_data, verbosity=0, message_text="No Response", dl_reg=False, stderr_flag=False):
"""Prints or logs parsed MessageId response based on verbosity level and returns the
following message information in a list:
* MessageArgs
* MessageId
* RestResponse status
* Resolution
* Full error message text
:param response_data: message response of a call.
:type response_data: :class:`redfish.rest.containers.RestResponse`
:param verbosity: Optional verbosity level. Only modifies what is output to log or screen.
:type verbosity: int
:param message_text: Response message text. If not provided, message_handler will attempt to
parse it from the RestResponse and registries.
:type message_text: str
:param dl_reg: Flag to download registry. If this is set to True a generic message response
will be returned instead of gathered from registries.
:type dl_reg: bool
:returns: List of error response dictionaries.
"""
_tmp_message_id = _tmp_description = _tmp_resolution = message_text
retlist = list()
response_error_str = ""
try:
response_status = response_data.status
except (AttributeError, ValueError):
response_status = "???"
try:
response_data = response_data.dict
except (AttributeError, ValueError):
pass
try:
for inst in self.get_message_data(response_data, dl_reg):
try:
for _key in inst.keys():
if "messageid" in str(_key.lower()):
_tmp_message_id = inst[_key]
if "description" in str(_key.lower()):
_tmp_description = inst[_key]
if inst.get("Message") and inst.get("MessageArgs"):
for i in range(inst["Message"].count("%")):
inst["Message"] = inst["Message"].replace(
"%" + str(i + 1), '"' + inst["MessageArgs"][i] + '"'
)
message_text = inst.get("Message", " ")
elif inst.get("Message"):
message_text = inst.get("Message", " ")
elif response_status not in [200, 201]:
message_text = _tmp_message_id
_tmp_resolution = inst.get("Resolution", " ")
except (KeyError, ValueError, TypeError):
pass
finally:
response_error_str += "[%s] %s\n" % (response_status, message_text)
print_handler(
self.verbosity_levels(
message_text,
_tmp_message_id,
_tmp_description,
_tmp_resolution,
response_status,
verbosity,
dl_reg,
),
stderr_flag,
)
retlist.append(inst)
except Exception:
if not message_text:
message_text = _tmp_message_id
response_error_str += "[%s] %s\n" % (response_status, message_text)
print_handler(
self.verbosity_levels(
message_text,
_tmp_message_id,
_tmp_description,
_tmp_resolution,
response_status,
verbosity,
dl_reg,
)
)
retlist.append(inst)
finally:
return retlist
def get_message_data(self, resp_data, dl_reg=False):
"""Obtain relevant keys from rest response.
:param resp: response
:type resp: :class:`redfish.rest.containers.RestResponse`
:returns: list of error response dictionaries
"""
err_response_keys = ["MessageId", "Message", "MessageArgs", "Resolution"]
try:
if "messageid" in [_key.lower() for _key in resp_data.keys()]:
data_extract = [resp_data]
else:
raise TypeError
except (TypeError, KeyError):
data_extract = json_traversal(resp_data, "messageid", ret_dict=True)
if data_extract:
try:
if not dl_reg:
for inst in data_extract:
if [key.lower() for key in inst.keys()] not in [erk.lower() for erk in err_response_keys]:
# Find the key that matches 'messageid' (case-insensitive)
for key in inst.keys():
if key.lower() == "messageid":
inst.update(self.get_error_messages(inst[key]))
break
finally:
return data_extract
else:
return None
def verbosity_levels(
self,
message,
messageid=" ",
description=" ",
resolution=" ",
response_status=None,
verbosity=0,
dl_reg=False,
):
"""Formatting based on verbosity level.
:param message: Message from BMC response combined with the registry model/schema.
:type message: str
:param messageid: Error code as classified by the BMC's error code registry.
:type messageid: str
:param resolution: Message from BMC registry model/schema with the suggested
resolution for the given error.
:type resolution: str
:param response_status: HTTP response status code.
:type response_status: int
:param verbosity: Option to set/control output message (stderr) verbosity.
:type verbosity: int
:returns: Message to be returned to caller.
"""
resp_str = ""
if response_status:
resp_str = "[" + str(response_status) + "] "
if (verbosity == 1 or dl_reg) and message:
return resp_str + message + "\n"
elif verbosity > 1 and messageid and message and resolution:
if not resp_str:
resp_str = "None "
return (
"\nHTTP Response Code: "
+ resp_str[:-1]
+ "\nMessageId: "
+ messageid
+ "\nDescription: "
+ description
+ "\nMessage: "
+ message
+ "\nResolution: "
+ resolution
+ "\n"
)
else:
if response_status == 400:
return "" + message + "\n"
else:
return ""
# unused? (removal pending)
@staticmethod
def _get_errmsg_type(results):
"""Return the registry type of a response.
:param resuts: rest response.
:type results: RestResponse.
:returns: returns a Registry Id type string, None if not match is found, or no_id if the
response is not an error message.
"""
return get_errmsg_type(results)
def get_error_messages(self, regtype=None):
"""Returns registry error messages. Can specify a specific registry to return by Id.
:param regtype: Id of registry type to add to list.
:type regtype: str
:returns: A list of error messages.
"""
LOGGER.info("Entering validation...")
messages = None
errmessages = {}
reglist = []
# An error occurred during the shortcut method so let's go through each registry,
# obtain the schema and narrow down the selected schema for the registry type provided
try:
_regtype = regtype.split(".")[0]
for reg in self.validation_mgr.iterregmems():
# gen 10 / gen 9 rest
if _regtype:
if reg and "Id" in reg and reg["Id"] == _regtype:
try:
reglist.append(reg["Registry"])
except KeyError:
reglist.append(reg["Schema"])
break
else:
continue
if not reglist:
# gen 9 redfish
regval = [reg.get(arg, None) for arg in ["Registry", "Schema", "Id"]]
regval = next(
(val for val in regval if val and "biosattributeregistry" not in val),
None,
)
if not regval and reg:
reg = reg["@odata.id"].split("/")
reg = reg[len(reg) - 2]
if "biosattributeregistry" not in reg.lower():
reglist.append(reg)
elif regval:
reglist.append(regval)
for reg in reglist:
reg = reg.replace("%23", "#")
messages = self.validation_mgr.get_registry_model(
getmsg=True, currtype=reg, searchtype=self.msg_reg_type
)
if messages:
errmessages.update(messages.get(next(iter(messages)))[regtype.split(".")[-1]])
if not reglist or not errmessages:
raise Exception
except Exception:
raise EmptyRaiseForEAFP("Unable to find registry schema with provided registry " "type: %s" % regtype)
else:
return errmessages
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,261 @@
###
# 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 -*-
"""A threaded version of RIS _load for quicker searching"""
# ---------Imports---------
import logging
import threading
from queue import Empty
import jsonpath_rw
# Added for py3 compatibility
import six
from six.moves.urllib.parse import urlparse, urlunparse
import redfish.ris
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
# ---------End of debug logger---------
class LoadWorker(threading.Thread):
"""A threaded implementation of _load for quicker crawling"""
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
self.exception = None
def run(self):
"""Main worker function"""
try:
while True:
(
path,
includelogs,
loadcomplete,
crawl,
rel,
init,
prevpath,
originaluri,
theobj,
) = self.queue.get()
if (
path
== includelogs
== loadcomplete
== crawl
== rel
== init
== prevpath
== originaluri
== theobj
== "KILL"
):
break
if path.endswith("?page=1") and not loadcomplete:
# Don't download schemas in crawl unless we are loading absolutely everything
self.queue.task_done()
continue
elif path == "/" or "telemetry" in path.lower() or not path.startswith("/redfish/v1"):
self.queue.task_done()
continue
elif not includelogs:
# Only include logs when asked as there can be an extreme amount of entries
if "/log" in path.lower():
self.queue.task_done()
continue
# TODO: need to find a better way to support non ascii characters
path = path.replace("|", "%7C")
# remove fragments
newpath = urlparse(path)
newpath = list(newpath[:])
newpath[-1] = ""
path = urlunparse(tuple(newpath))
if prevpath and prevpath != path:
theobj.ctree[prevpath].update([path])
if not rel:
if path.lower() in theobj.visited_urls:
self.queue.task_done()
continue
LOGGER.debug("_loading %s", path)
resp = theobj._client.get(path)
if resp.status != 200 and path.lower() == theobj.typepath.defs.biospath:
self.queue.task_done()
raise redfish.ris.ris.BiosUnregisteredError()
elif resp.status == 401:
self.queue.task_done()
raise redfish.ris.ris.SessionExpired(
"Invalid session. Please logout and " "log back in or include credentials."
)
elif resp.status not in (201, 200):
theobj.removepath(path)
self.queue.task_done()
continue
theobj.update_member(resp=resp, path=path, init=init)
fpath = lambda pa, path: (
path
if pa.endswith(theobj.typepath.defs.hrefstring) and pa.startswith((theobj.collstr, "Entries"))
else None
)
# follow all the href attributes
if theobj.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
theobj.get_queue.put(
(
href,
includelogs,
loadcomplete,
crawl,
rel,
init,
None,
originaluri,
theobj,
)
)
else:
next_link_uri = path + "?page=" + str(resp.dict["links"]["NextPage"]["page"])
href = "%s" % next_link_uri
theobj.get_queue.put(
(
href,
includelogs,
loadcomplete,
crawl,
rel,
init,
None,
path,
theobj,
)
)
# Only use monolith if we are set to
matchrdirpath = (
next(
(match for match in matches if match.value == theobj._resourcedir),
None,
)
if theobj.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
theobj.get_queue.put(
(
href,
includelogs,
loadcomplete,
crawl,
rel,
init,
fpath(str(match.full_path), path),
originaluri,
theobj,
)
)
elif crawl:
href = "%s" % matchrdirpath.value
theobj.get_queue.put(
(
href,
includelogs,
loadcomplete,
crawl,
rel,
init,
path,
originaluri,
theobj,
)
)
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):
theobj.get_queue.put(
(
match.value,
includelogs,
loadcomplete,
crawl,
rel,
init,
fpath(str(match.full_path), path),
originaluri,
theobj,
)
)
self.queue.task_done()
except Empty:
pass
except Exception as excp:
self.exception = excp
def get_exception(self):
"""Get any exception from the thread"""
return self.exception
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,15 @@
from rmc import RmcApp
class Rmc_API(RmcApp):
def __init__(self, Args=None):
RmcApp.__init__(self, Args)
def Api_login(self, url=None, username=None, password=None):
self.login(base_url=url, username=username, password=password)
# for testing
if __name__ == "__main__":
api = Rmc_API()
api.do_stuff()
@@ -0,0 +1,452 @@
###
# 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 -*-
"""RMC helper file. Includes RMC errors and caching functionality for monolith."""
# ---------Imports---------
import errno
import hashlib
import json
import logging
import os
from redfish.rest.containers import RestRequest, StaticRestResponse
from redfish.rest.v1 import RestClient
from .ris import RisMonolith
from .sharedtypes import JSONEncoder
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
# ---------End of debug logger---------
class RdmcError(Exception):
"""Base class for all RDMC Exceptions"""
errcode = 1
def __init__(self, message):
Exception.__init__(self, message)
class InvalidCommandLineError(RdmcError):
"""Raised when user enter incorrect command line arguments"""
pass
class TfaEnablePreRequisiteError(RdmcError):
"""Raised when pre-requisites not met while enabling TFA"""
pass
class FailureDuringCommitError(RdmcError):
"""Raised when there is an error while committing."""
pass
class UserNotAdminError(RdmcError):
"""Raised when user doesn't have admin priviledges, but they are required."""
pass
class IncompatibleiLOVersionError(RdmcError):
"""Raised when iLO version is incompatible."""
pass
class UndefinedClientError(Exception):
"""Raised when there are no clients active (usually when user hasn't logged in)."""
pass
class InstanceNotFoundError(Exception):
"""Raised when attempting to select an instance that does not exist."""
pass
class CurrentlyLoggedInError(Exception):
"""Raised when attempting to select an instance that does not exist"""
pass
class IloLicenseError(Exception):
"""Raised when the proper iLO license is not available for a command"""
pass
class ScepenabledError(Exception):
"""Raised when the generation csr or deletion of https cert is issues when scep is enabled"""
pass
class NothingSelectedError(Exception):
"""Raised when attempting to access an object without first selecting it."""
pass
class NothingSelectedFilterError(Exception):
"""Raised when the filter applied doesn't match any selection (general)."""
pass
class NothingSelectedSetError(Exception):
"""Raised when attempting to access an object without first selecting it (In set)."""
pass
class InvalidSelectionError(Exception):
"""Raised when selection argument fails to match anything."""
pass
class IdTokenError(Exception):
"""Raised when user is not authorized to complete the operation."""
pass
class ValueChangedError(Exception):
"""Raised if user tries to set/commit a value when monolith has older data."""
pass
class LoadSkipSettingError(Exception):
"""Raised when one or more settings are absent in given server."""
pass
class InvalidPathError(Exception):
"""Raised when requested path is not found."""
pass
class UnableToObtainIloVersionError(Exception):
"""Raised when iloversion is missing from default path."""
pass
class ValidationError(Exception):
"""Raised when there is a problem with user input."""
def __init__(self, errlist):
super(ValidationError, self).__init__(errlist)
self._errlist = errlist
def get_errors(self):
"""Returns error list."""
return self._errlist
class IloResponseError(Exception):
"""Raised when iLO returns with a non 2XX response."""
pass
class EmptyRaiseForEAFP(Exception):
"""Raised when you need to check for issues and take different action."""
pass
class IncorrectPropValue(Exception):
"""Raised when you pass an incorrect value to for the associated property."""
pass
class RmcCacheManager(object):
"""Manages caching/uncaching of data for RmcApp.
:param rmc: RmcApp to be managed
:type rmc: :class:`redfish.ris.rmc.RmcApp`
"""
def __init__(self, rmc):
"""Initialize RmcCacheManager
:param rmc: RmcApp to be managed
:type rmc: RmcApp object
"""
self._rmc = rmc
self.encodefunct = lambda data: data
self.decodefunct = lambda data: data
class RmcFileCacheManager(RmcCacheManager):
"""RMC file cache manager.
:param rmc: RmcApp to be managed
:type rmc: :class:`redfish.ris.rmc.RmcApp`
"""
def __init__(self, rmc):
super(RmcFileCacheManager, self).__init__(rmc)
def logout_del_function(self, url=None):
"""Searches for a specific url in cache or returns all urls and returns them for RmcApp
to run logout on, clearing the session.
:param url: The URL to pass back for logout.
:type url: str
"""
if self._rmc.cache:
cachedir = self._rmc.cachedir
indexfn = os.path.join(cachedir, "index") # %s\\index' % cachedir
else:
indexfn = ""
sessionlocs = []
if os.path.isfile(indexfn):
try:
indexfh = open(indexfn, "r")
index_cache = json.load(indexfh)
indexfh.close()
for index in index_cache:
if url:
if url in index["url"]:
os.remove(os.path.join(cachedir, index["href"]))
break
else:
if os.path.isfile(os.path.join(cachedir, index["href"])):
monolith = open(os.path.join(cachedir, index["href"]), "r")
data = json.load(monolith)
monolith.close()
for item in data:
if "login" in item and "session_location" in data["login"]:
if "blobstore" in data["login"]["url"]:
loc = data["login"]["session_location"].split("//")[-1]
sesurl = None
else:
loc = None
if data["login"]["session_location"] is not None:
loc = data["login"]["session_location"].split(data["login"]["url"])[-1]
sesurl = data["login"]["url"]
sessionlocs.append(
(
loc,
sesurl,
self._rmc._cm.decodefunct(data["login"]["session_key"]),
)
)
os.remove(os.path.join(cachedir, index["href"]))
except BaseException as excp:
LOGGER.warning("Unable to read cache data %s", excp)
return sessionlocs
def uncache_rmc(self, creds=None, enc=False):
"""Uncaches monolith data from cache location specified by RmcApp.
:param creds: Dictionary of username and password.
Only required for restoring high security local calls.
:type creds: dict
:param enc: Flag if credentials passed are encoded.
:type enc: bool
"""
cachedir = self._rmc.cachedir
indexfn = "%s/index" % cachedir
if os.path.isfile(indexfn):
try:
indexfh = open(indexfn, "r")
index_cache = json.load(indexfh)
indexfh.close()
for index in index_cache:
clientfn = index["href"]
self._uncache_client(clientfn, creds=creds, enc=enc)
except BaseException as excp:
LOGGER.warning("Unable to read cache data %s", excp)
def _uncache_client(self, cachefn, creds=None, enc=False):
"""Monolith uncache function for parsing and passing all client data and associated
credential attributes.
:param cachefn: The cache file name.
:type cachefn: str.
"""
cachedir = self._rmc.cachedir
clientsfn = "%s/%s" % (cachedir, cachefn)
if os.path.isfile(clientsfn):
try:
clientsfh = open(clientsfn, "r")
client = json.load(clientsfh)
clientsfh.close()
if "login" not in client:
return
login_data = client["login"]
if "url" not in login_data:
return
self._rmc.typepath.getgen(
login_data.get("ilo"),
url=login_data.get("url"),
isredfish=login_data.get("redfish", None),
ca_cert_data=login_data.get("ca_cert_data", {}),
)
if creds and login_data.get("url", "").startswith("blobstore://"):
if enc:
creds["username"] = self._rmc._cm.decodefunct(creds["username"])
creds["password"] = self._rmc._cm.decodefunct(creds["password"])
login_data["username"] = creds["username"]
login_data["password"] = creds["password"]
if isinstance(login_data["username"], bytes):
login_data["username"] = login_data["username"].decode("utf-8")
if isinstance(login_data["password"], bytes):
login_data["password"] = login_data["password"].decode("utf-8")
redfishinst = RestClient(
username=login_data.get("username", "Administrator"),
password=login_data.get("password", None),
base_url=login_data.get("url", None),
biospassword=login_data.get("bios_password", None),
is_redfish=login_data.get("redfish", None),
default_prefix=self._rmc.typepath.defs.startpath,
proxy=login_data.get("proxy", None),
ca_cert_data=login_data.get("ca_cert_data", {}),
)
if login_data.get("authorization_key"):
redfishinst.basic_auth = login_data.get("authorization_key")
elif login_data.get("session_key"):
redfishinst.session_key = self._rmc._cm.decodefunct(login_data.get("session_key"))
if isinstance(redfishinst.session_key, bytes):
redfishinst.session_key = redfishinst.session_key.decode("utf-8")
redfishinst.session_location = login_data.get("session_location")
if "selector" in client:
self._rmc.selector = client["selector"]
if login_data.get("iloversion"):
redfishinst.iloversion = login_data.get("iloversion")
else:
redfishinst.iloversion = None
self._rmc.typepath.iloversion = redfishinst.iloversion
getdata = client["get"]
for key in list(getdata.keys()):
if key == redfishinst.default_prefix:
restreq = RestRequest(method="GET", path=key)
getdata[key]["restreq"] = restreq
redfishinst.root = StaticRestResponse(**getdata[key])
break
self._rmc.monolith = RisMonolith(redfishinst, self._rmc.typepath)
self._rmc.monolith.load_from_dict(client["monolith"])
self._rmc.redfishinst = redfishinst
# make sure root is there
_ = redfishinst.root
self._rmc.typepath.defineregschemapath(redfishinst.root.dict)
except BaseException as excp:
LOGGER.warning("Unable to read cache data %s", excp)
def cache_rmc(self):
"""Saves monolith data to the file path specified in RmcApp."""
if not self._rmc.cache:
return
cachedir = self._rmc.cachedir
if not os.path.isdir(cachedir):
try:
os.makedirs(cachedir)
except OSError as ex:
if ex.errno == errno.EEXIST:
pass
else:
raise
index_map = dict()
index_cache = list()
if self._rmc.redfishinst:
shaobj = hashlib.new("SHA256")
shaobj.update(self._rmc.redfishinst.base_url.encode("utf-8"))
md5str = shaobj.hexdigest()
index_map[self._rmc.redfishinst.base_url] = md5str
index_data = dict(
url=self._rmc.redfishinst.base_url,
href="%s" % md5str,
)
index_cache.append(index_data)
indexfh = open("%s/index" % cachedir, "w")
json.dump(index_cache, indexfh, indent=2, cls=JSONEncoder)
indexfh.close()
if self._rmc.redfishinst:
login_data = dict(
username=None,
password=None,
url=self._rmc.redfishinst.base_url,
session_key=self._rmc._cm.encodefunct(self._rmc.redfishinst.session_key),
session_location=self._rmc.redfishinst.session_location,
authorization_key=self._rmc.redfishinst.basic_auth,
bios_password=self._rmc.redfishinst.bios_password,
redfish=self._rmc.monolith.is_redfish,
ilo=self._rmc.typepath.ilogen,
iloversion=self._rmc.typepath.iloversion,
proxy=self._rmc.redfishinst.proxy,
ca_cert_data=self._rmc.redfishinst._cert_data if self._rmc.redfishinst._cert_data else dict(),
)
clients_data = dict(
selector=self._rmc.selector,
login=login_data,
monolith=self._rmc.monolith,
get=self._rmc.monolith.paths,
)
clientsfh = open("%s/%s" % (cachedir, index_map[self._rmc.redfishinst.base_url]), "w")
if isinstance(clients_data, bytes):
clients_data = clients_data.decode("utf-8")
json.dump(clients_data, clientsfh, indent=2, cls=JSONEncoder)
clientsfh.close()
@@ -0,0 +1,61 @@
###
# 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 -*-
"""Shared types used in this library."""
# ---------Imports---------
import logging
import jsonpatch
from redfish.rest.containers import JSONEncoder
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
# ---------End of debug logger---------
class JSONEncoder(JSONEncoder):
"""Custom JSONEncoder that understands our types"""
def default(self, obj):
"""Set defaults
:param obj: json object.
:type obj: str.
"""
if isinstance(obj, Dictable):
return obj.to_dict()
elif isinstance(obj, set):
return list(obj)
elif isinstance(obj, jsonpatch.JsonPatch):
return obj.patch
return super(JSONEncoder, self).default(obj)
class Dictable(object):
"""A base class which adds the to_dict method used during json encoding"""
def to_dict(self):
"""Overridable funciton"""
raise RuntimeError("You must override this method in your derived class")
@@ -0,0 +1,558 @@
###
# 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
File diff suppressed because it is too large. Load diff