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,19 @@
""" Redfish restful library """
__all__ = ["rest", "ris", "hpilo"]
__version__ = "7.0.0.0"
import logging
from redfish.rest.v1 import AuthMethod, LegacyRestClient, RedfishClient
def redfish_logger(file_name, log_format, log_level=logging.ERROR):
"""redfish logger"""
formatter = logging.Formatter(log_format)
fhdl = logging.FileHandler(file_name)
fhdl.setFormatter(formatter)
logger = logging.getLogger(__name__)
logger.addHandler(fhdl)
logger.setLevel(log_level)
return logger
@@ -0,0 +1,3 @@
"""
Utilities to simplify communicating with iLO locally
"""
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,284 @@
###
# 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 -*-
"""Base implementation for interaction with the iLO interface"""
# ---------Imports---------
import logging
import os
import struct
import time
from ctypes import byref, c_uint32, c_char_p, c_void_p, create_string_buffer
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
# ---------End of debug logger---------
class BlobReturnCodes(object):
"""Blob store return codes.
SUCCESS success
"""
SUCCESS = 0
if os.name != "nt":
CHIFERR_NoDriver = 19
CHIFERR_AccessDenied = 13
else:
CHIFERR_NoDriver = 2
CHIFERR_AccessDenied = 5
class HpIloInitialError(Exception):
"""Raised when error during initialization of iLO Chif channel"""
pass
class HpIloNoChifDriverError(Exception):
"""Raised when error during initialization of iLO Chif channel"""
pass
class HpIloChifAccessDeniedError(Exception):
"""Raised when error during initialization of iLO Chif channel"""
pass
class HpIloPrepareAndCreateChannelError(Exception):
"""Raised when error during initialization of iLO Chif channel"""
pass
class HpIloChifPacketExchangeError(Exception):
"""Raised when errors encountered when exchanging chif packet"""
pass
class HpIloReadError(Exception):
"""Raised when errors encountered when reading from iLO"""
pass
class HpIloWriteError(Exception):
"""Raised when errors encountered when writing to iLO"""
pass
class HpIloSendReceiveError(Exception):
"""Raised when errors encountered when reading form iLO after sending"""
pass
class HpIloNoDriverError(Exception):
"""Raised when errors encountered when there is no ilo driver"""
pass
class HpIlo(object):
"""Base class of interaction with iLO"""
def __init__(self, dll=None, log_dir=None):
fhandle = c_void_p()
self.dll = dll
self.log_dir = log_dir
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Initializing with DLL: %s and log directory: %s", dll, log_dir)
self.dll.enabledebugoutput.argtypes = [c_char_p]
if log_dir is not None:
logdir_c = create_string_buffer(log_dir.encode("UTF-8"))
LOGGER.debug("Enabling debug output with log directory: %s", log_dir)
self.dll.enabledebugoutput(logdir_c)
try:
LOGGER.debug("Calling ChifInitialize...")
self.dll.ChifInitialize(None)
self.dll.ChifCreate.argtypes = [c_void_p]
self.dll.ChifCreate.restype = c_uint32
LOGGER.debug("Calling ChifCreate...")
status = self.dll.ChifCreate(byref(fhandle))
if status != BlobReturnCodes.SUCCESS:
LOGGER.error("Error occurred while creating channel. Error code: %s", status)
raise HpIloInitialError(f"Error {status} occurred while trying to create a channel.")
self.fhandle = fhandle
LOGGER.debug("Channel created successfully with handle: %s", fhandle)
if "skip_ping" not in os.environ:
LOGGER.debug("Attempting to ping iLO...")
status = self.dll.ChifPing(self.fhandle)
if status != BlobReturnCodes.SUCCESS:
errmsg = f"Error {status} occurred while trying to open a channel to iLO"
if status == BlobReturnCodes.CHIFERR_NoDriver:
errmsg = "chif"
elif status == BlobReturnCodes.CHIFERR_AccessDenied:
errmsg = "You must be root/Administrator to use this program."
LOGGER.error("Ping failed: %s", errmsg)
raise HpIloInitialError(errmsg)
LOGGER.debug("Ping to iLO successful.")
# Setting receive timeout
LOGGER.debug("Setting receive timeout to 60000ms...")
self.dll.ChifSetRecvTimeout(self.fhandle, 60000)
LOGGER.debug("Receive timeout set successfully.")
except Exception as e:
LOGGER.error("An error occurred during initialization: %s", str(e))
raise
def chif_packet_exchange(self, data):
"""Function for handling chif packet exchange
:param data: data to be sent for packet exchange
:type data: str
"""
LOGGER.info("Starting chif packet exchange...")
LOGGER.debug("Data ready to be sent...")
# LOGGER.debug(f"Data to be sent: {data[:50]}... (first 50 bytes)")
datarecv = self.dll.get_max_buffer_size()
buff = create_string_buffer(bytes(data))
recbuff = create_string_buffer(datarecv)
try:
error = self.dll.ChifPacketExchange(self.fhandle, byref(buff), byref(recbuff), datarecv)
except Exception as e:
LOGGER.error(f"Error occurred while calling ChifPacketExchange: {e}")
raise
if error != BlobReturnCodes.SUCCESS:
if error == 8:
raise HpIloChifPacketExchangeError(
"High security mode or Host Authentication has been enabled, Please provide valid credentials \n "
)
else:
raise HpIloChifPacketExchangeError(f"Error {error} occurred while exchanging chif packet")
LOGGER.info("Chif packet exchange successful.")
# Process received packet
pkt = bytearray()
if datarecv is None:
LOGGER.warning("Received buffer is empty or None.")
pkt = bytearray(recbuff[:])
else:
pkt = bytearray(recbuff[:datarecv])
LOGGER.debug("Received data...")
# LOGGER.debug(f"Received data: {pkt[:50]}... (first 50 bytes)")
return pkt
def send_receive_raw(self, data, retries=10):
"""Function implementing proper send receive retry protocol
:param data: data to be sent for packet exchange
:type data: str
:param retries: number of retries for reading data from iLO
:type retries: int
"""
tries = 0
sequence = struct.unpack("<H", bytes(data[2:4]))[0]
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Retries: %d, Initial sequence: %d", retries, sequence)
while tries < retries:
try:
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Attempt %d, sending data to iLO", tries + 1)
resp = self.chif_packet_exchange(data)
# Log the response data for debugging (consider limiting if large)
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Received response...")
# Check for matching sequence
received_sequence = struct.unpack("<H", bytes(resp[2:4]))[0]
if sequence != received_sequence:
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug(
"Attempt %d: Bad sequence number. Expected: %d, Got: %d",
tries + 1,
sequence,
received_sequence,
)
continue # Retry with a new attempt
# Successfully received response with correct sequence
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Attempt %d: Successful response with correct sequence.", tries + 1)
return resp
except Exception as excp:
time.sleep(1)
# Log exception on each retry attempt
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Attempt %d: Error while reading iLO: %s", tries + 1, str(excp))
if tries == (retries - 1):
self.close()
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Final attempt failed. Closing connection.")
raise excp # Raise after final attempt
tries += 1
# If retries exhausted, log and raise custom exception
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug("Retries exhausted. iLO not responding after %d attempts.", retries)
raise HpIloSendReceiveError("iLO not responding")
def close(self):
"""Chif close function"""
try:
if self.fhandle is not None:
LOGGER.debug("Calling ChifClose...")
self.dll.ChifClose(self.fhandle)
self.fhandle = None
except Exception:
pass
def __del__(self):
"""Chif delete function"""
self.close()
@@ -0,0 +1,428 @@
###
# Copyright 2024 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 -*-
"""Base implementation for interaction with vnic interface"""
from redfish.rest.connections import ChifDriverMissingOrNotFound, VnicNotEnabledError
from redfish.rest.v1 import InvalidCredentialsError
import logging
from ctypes import (
POINTER,
byref,
c_bool,
c_char_p,
c_int,
c_uint8,
c_wchar_p,
create_string_buffer,
)
from redfish.hpilo.risblobstore2 import BlobStore2
from redfish.hpilo.rishpilo import BlobReturnCodes
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
class GenerateAndSaveAccountError(Exception):
"""Raised when errors occured while generating and saving app account"""
pass
class ReactivateAppAccountTokenError(Exception):
"""Raised when errors occurred while reactivating app account"""
pass
class InactiveAppAccountTokenError(Exception):
"""Raised when inactive app account token"""
pass
class RemoveAccountError(Exception):
"""Raised when errors occured while removing app account"""
pass
class AppAccountExistsError(Exception):
"""Raised when errors occured while removing app account"""
pass
class VnicLoginError(Exception):
"""Raised when error occurs while VNIC login"""
pass
class AppIdListError(Exception):
"""Raised when error occurs during CompareAppIds"""
pass
class SavinginTPMError(Exception):
"""Raised when error occurs while saving app account in TPM"""
pass
class SavinginiLOError(Exception):
"""Raised when error occurs while saving app account in iLO"""
pass
class GenBeforeLoginError(Exception):
"""Raised when error occurs while getting iLO Gen before login"""
pass
class InvalidCommandLineError(Exception):
"""Raised when invalid command line arguments are passed"""
pass
class AppAccount(object):
def __init__(self, appname=None, appid=None, salt=None, username=None, password=None, log_dir=None):
"""Initialize the AppAccount object and configure logging."""
self.appname = (appname or "self_register").encode("utf-8")
self.dll = BlobStore2.gethprestchifhandle()
self.salt = (salt or "self_register").encode("utf-8")
self.username = username
self.password = password
self.log_dir = log_dir
LOGGER.debug(
f"Initialization parameters - AppName: {self.appname.decode('utf-8')}, "
f"Salt: {self.salt.decode('utf-8')}, Username: {self.username if self.username else 'None'}"
)
if log_dir and LOGGER.isEnabledFor(logging.DEBUG):
logdir_c = create_string_buffer(log_dir.encode("utf-8"))
self.dll.enabledebugoutput(logdir_c)
LOGGER.info("Debug output enabled. Logs stored in: %s", log_dir)
if appid and len(appid) == 4:
appid = self.ExpandAppId(appid)
self.appid = (appid or "self_register").encode("utf-8")
def ExpandAppId(self, appid):
"""Calls the DLL function to expand a 4-character app ID."""
self.dll.ExpandAppid.argtypes = [c_char_p, POINTER(c_char_p)]
self.dll.ExpandAppid.restype = c_int
host_app_id_c = c_char_p(appid.encode("utf-8"))
expanded_app_id_c = c_char_p(None)
LOGGER.debug(f"Calling ExpandAppid with AppID: {appid}")
ret = self.dll.ExpandAppid(host_app_id_c, byref(expanded_app_id_c))
if ret:
LOGGER.error(f"Failed to expand App ID {appid} (Return code: {ret})")
raise AppAccountExistsError()
expanded_id = expanded_app_id_c.value.decode("utf-8") if expanded_app_id_c else None
if LOGGER.isEnabledFor(logging.DEBUG):
LOGGER.debug(f"Successfully expanded App ID {appid} to {expanded_id}")
return expanded_id
def generate_and_save_apptoken(self):
"""Generate and save an application account, logging each step."""
LOGGER.info("Starting generate_and_save_apptoken process.")
self.dll.GenerateAppToken.argtypes = [c_char_p, c_char_p, c_char_p, c_char_p, c_char_p]
self.dll.GenerateAppToken.restype = c_int
self.username = self.username.encode("utf-8")
self.password = self.password.encode("utf-8")
LOGGER.debug("Calling GenerateAppToken for AppID: %s", self.appid.decode("utf-8"))
returncode = self.dll.GenerateAppToken(self.appid, self.appname, self.salt, self.username, self.password)
if returncode == BlobReturnCodes.SUCCESS:
LOGGER.info("Application account successfully generated and saved.")
return BlobReturnCodes.SUCCESS
elif returncode == -54:
LOGGER.error("Failed to save account in TPM.")
raise SavinginTPMError()
elif returncode == -53:
LOGGER.error("Failed to save account in iLO.")
raise SavinginiLOError()
elif returncode == -55:
LOGGER.warning("Application account already exists.")
raise AppAccountExistsError()
elif returncode == -8:
raise InvalidCredentialsError(0)
else:
LOGGER.critical("Unknown return code: %d", returncode)
raise GenerateAndSaveAccountError()
def reactivate_apptoken(self):
"""Reactivate an application account, logging each step."""
LOGGER.info("Starting reactivate_apptoken process.")
self.dll.ReactivateAppToken.argtypes = [c_char_p, c_char_p, c_char_p, c_char_p, c_char_p]
self.dll.ReactivateAppToken.restype = c_int
user_name = self.username.encode("utf-8")
password = self.password.encode("utf-8")
LOGGER.debug("Calling ReactivateAppToken for AppID: %s", self.appid.decode("utf-8"))
return_code = self.dll.ReactivateAppToken(self.appid, self.appname, self.salt, user_name, password)
if return_code == BlobReturnCodes.SUCCESS:
LOGGER.info("Application account has been reactivated successfully.")
return BlobReturnCodes.SUCCESS
elif return_code == -8:
LOGGER.error("Please enter valid credentials.")
raise InvalidCredentialsError(0)
else:
LOGGER.critical("Unknown return code: %d", return_code)
raise ReactivateAppAccountTokenError()
def remove_apptoken(self):
returncode = 1
if self.username and self.password:
LOGGER.debug(f"Using credentials to remove for AppID: {self.appid.decode('utf-8')}")
self.dll.DeleteAppTokenUsingCreds.argtypes = [c_char_p, c_char_p, c_char_p]
self.dll.DeleteAppTokenUsingCreds.restype = c_int
self.username = self.username.encode("utf-8")
self.password = self.password.encode("utf-8")
returncode = self.dll.DeleteAppTokenUsingCreds(self.appid, self.username, self.password)
elif self.appname and self.salt:
LOGGER.debug(f"Using app details to remove for AppID : {self.appid.decode('utf-8')}")
self.dll.DeleteAppToken.argtypes = [c_char_p, c_char_p, c_char_p]
self.dll.DeleteAppToken.restype = c_int
returncode = self.dll.DeleteAppToken(self.appid, self.appname, self.salt)
LOGGER.debug(f"Delete operation returned code: {returncode}")
if returncode == BlobReturnCodes.SUCCESS:
return BlobReturnCodes.SUCCESS
elif returncode == -8:
raise InvalidCredentialsError(0)
else:
raise RemoveAccountError("Error occurred while removing app account.")
def apptoken_exists(self):
"""Check if an application account exists in TPM."""
LOGGER.info("Checking if application account exists.")
self.dll.AppIdExistsinTPM.argtypes = [c_char_p, POINTER(c_bool)]
self.dll.AppIdExistsinTPM.restype = c_int
vexists = c_bool(False)
LOGGER.debug(f"Checking existence for AppID: {self.appid.decode('utf-8')}")
returncode = self.dll.AppIdExistsinTPM(self.appid, byref(vexists))
LOGGER.debug(f"AppIdExistsinTPM returned: {returncode}")
if returncode == BlobReturnCodes.SUCCESS:
LOGGER.info("app account existence check completed. Exists: %s", vexists.value)
return vexists.value
else:
LOGGER.error("Failed to check account existence")
LOGGER.debug(f"Failed with return code: {returncode}")
raise AppAccountExistsError()
def vlogin(self):
LOGGER.info("Starting VNIC login process.")
self.dll.GetSessionID.argtypes = [c_char_p, c_char_p, c_char_p, POINTER(c_char_p), POINTER(c_char_p)]
self.dll.GetSessionID.restype = c_int
session_id = c_char_p()
session_location = c_char_p()
errorcode = 0
LOGGER.debug(f"Attempting login for AppID: {self.appid.decode('utf-8')}")
try:
errorcode = self.dll.GetSessionID(
self.appid, self.appname, self.salt, byref(session_id), byref(session_location)
)
LOGGER.debug(f"GetSessionID returned code: {errorcode}")
if errorcode == 2:
raise InactiveAppAccountTokenError()
elif errorcode != BlobReturnCodes.SUCCESS:
raise Exception
session_id = session_id.value.decode()
session_location = session_location.value.decode()
session_location = "/" + session_location.split("/", 3)[-1]
LOGGER.debug("Successfully decoded session information")
LOGGER.info(
"VNIC login successful. Session Location: %s, Session ID: ****%s", session_location, session_id[-4:]
)
return session_id, session_location
except InactiveAppAccountTokenError:
LOGGER.error("Login failed due to an inactive or expired App Account token.")
raise InactiveAppAccountTokenError()
except Exception as excp:
LOGGER.error("VNIC login failed: %s", str(excp))
raise VnicLoginError()
def vnic_exists(self):
"""Check if the VNIC is ready for use."""
LOGGER.info("Checking if VNIC is ready for use.")
self.dll.ReadyToUse.restype = c_int
LOGGER.debug("Calling ReadyToUse")
status = self.dll.ReadyToUse()
LOGGER.debug(f"ReadyToUse returned status: {status}")
if status == BlobReturnCodes.SUCCESS:
LOGGER.info("VNIC is ready for use.")
return True
else:
LOGGER.warning("VNIC is not ready for use.")
return False
def version_beforelogin(self):
"""Detect the iLO version before login."""
LOGGER.info("Detecting iLO version before login.")
self.dll.DetectILO.argtypes = [c_char_p, POINTER(c_int), POINTER(c_int)]
self.dll.DetectILO.restype = c_int
ilo_type = c_int(0)
security_state = c_int(0)
if not self.appid:
self.appid = "self_register".encode("utf-8")
returncode = self.dll.DetectILO(self.appid, byref(ilo_type), byref(security_state))
LOGGER.debug(f"DetectILO returned: type={ilo_type.value}, security={security_state.value}, code={returncode}")
if returncode == BlobReturnCodes.SUCCESS:
LOGGER.info(
"iLO version successfully detected: %d, Security State: %d", ilo_type.value, security_state.value
)
return ilo_type.value, security_state.value
elif returncode == -1 and ilo_type.value == 100:
LOGGER.info(
"Detectilo returned ilotype: %d, Security State: %d, " "returncode: %d",
ilo_type.value,
security_state.value,
returncode,
)
raise ChifDriverMissingOrNotFound()
elif returncode == -50 and ilo_type.value == 101:
LOGGER.info(
"Detectilo returned ilotype: %d, Security State: %d, " "returncode: %d",
ilo_type.value,
security_state.value,
returncode,
)
raise VnicNotEnabledError()
elif returncode == -2:
LOGGER.info("Invalid parameters were passed in the command line. Detectilo iLO returncode: -2")
raise InvalidCommandLineError()
else:
LOGGER.info(
"Detectilo returned ilotype: %d, Security State: %d, " "returncode: %d",
ilo_type.value,
security_state.value,
returncode,
)
LOGGER.error("Failed to detect iLO version.")
raise GenBeforeLoginError()
def GetIPAddress(self):
"""Retrieve the IP address of the iLO interface."""
LOGGER.info("Retrieving IP address.")
LOGGER.debug("Calling GetIPAddress")
self.dll.GetIPAddress.restype = c_wchar_p
ip_addr = self.dll.GetIPAddress()
LOGGER.debug(f"GetIPAddress returned: {ip_addr if ip_addr else 'None'}")
LOGGER.info("IP address retrieved: %s", ip_addr)
return ip_addr
def CompareAppIds(self):
"""Compare application IDs and return a structured dictionary."""
LOGGER.info("Starting CompareAppIds process.")
self.dll.CompareAppIds.argtypes = [
c_char_p,
c_char_p,
c_char_p,
POINTER(POINTER(c_char_p)),
POINTER(POINTER(c_char_p)),
POINTER(c_int),
POINTER(POINTER(c_uint8)),
POINTER(POINTER(c_uint8)),
]
self.dll.CompareAppIds.restype = c_int
app_ids_ptr = POINTER(c_char_p)()
app_names_ptr = POINTER(c_char_p)()
app_id_count = c_int()
in_tpm_ptr = POINTER(c_uint8)()
in_ilo_ptr = POINTER(c_uint8)()
LOGGER.debug(
"Calling CompareAppIds with AppID: %s, AppName: %s",
self.appid.decode("utf-8"),
self.appname.decode("utf-8"),
)
returncode = self.dll.CompareAppIds(
self.appid,
self.appname,
self.salt,
byref(app_ids_ptr),
byref(app_names_ptr),
byref(app_id_count),
byref(in_tpm_ptr),
byref(in_ilo_ptr),
)
if returncode != BlobReturnCodes.SUCCESS:
LOGGER.error("Error occurred in CompareAppIds. Return Code: %d", returncode)
raise AppIdListError("Error occurred while retrieving App Id information.\n")
LOGGER.info("CompareAppIds executed successfully. Number of App IDs: %d", app_id_count.value)
app_ids = [app_ids_ptr[i].decode("utf-8") for i in range(app_id_count.value)]
app_names = [app_names_ptr[i].decode("utf-8") for i in range(app_id_count.value)]
in_tpm_list = [bool(in_tpm_ptr[i]) for i in range(app_id_count.value)]
in_ilo_list = [bool(in_ilo_ptr[i]) for i in range(app_id_count.value)]
appid_dict = list()
for i in range(app_id_count.value):
appid_dict.append(
{
"ApplicationID": app_ids[i],
"ApplicationName": app_names[i],
"ExistsInTPM": in_tpm_list[i],
"ExistsIniLO": in_ilo_list[i],
}
)
LOGGER.info("CompareAppIds completed successfully. Processed %d AppIDs.", len(appid_dict))
return appid_dict
@@ -0,0 +1,2 @@
""" Base interface to simplify interaction with LegacyRest/Redfish data and Remote/Local
connections."""
@@ -0,0 +1,624 @@
###
# 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 -*-
"""All Connections for interacting with REST."""
import gzip
import json
import logging
import time
import urllib3
from urllib3 import PoolManager, ProxyManager
from urllib3.exceptions import DecodeError, MaxRetryError
try:
urllib3.disable_warnings()
from urllib3.contrib.socks import SOCKSProxyManager
except ImportError:
pass
import six
from six import BytesIO
from six.moves.urllib.parse import urlencode, urlparse
from redfish.hpilo.risblobstore2 import (
Blob2OverrideError,
Blob2SecurityError,
BlobStore2,
)
from redfish.hpilo.rishpilo import HpIloChifPacketExchangeError
from redfish.rest.containers import RestRequest, RestResponse, RisRestResponse
from redfish.security_masking import SecurityMasker
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
# ---------End of debug logger---------
class RetriesExhaustedError(Exception):
"""Raised when retry attempts have been exhausted."""
pass
class VnicNotEnabledError(Exception):
"""Raised when retry attempts have been exhausted when VNIC is not enabled."""
pass
class DecompressResponseError(Exception):
"""Raised when decompressing the response failed."""
pass
class InvalidCredentialsError(Exception):
"""Raised when invalid credentials have been provided."""
pass
class InvalidCertificateError(Exception):
"""Raised when a invalid certificate has been provided."""
pass
class ChifDriverMissingOrNotFound(Exception):
"""Raised when CHIF driver is missing or not found."""
pass
class SecurityStateError(Exception):
"""Raised when there is a strict security state without authentication."""
pass
class OneTimePasscodeError(Exception):
"""Raised when OTP is sent to the registered email."""
pass
class TokenExpiredError(Exception):
"""Raised when OTP entered has expired."""
pass
class UnauthorizedLoginAttemptError(Exception):
"""Raised when Login is Unauthorized"""
pass
class HttpConnection(object):
"""HTTP connection capable of authenticating with HTTPS and Http/Socks Proxies
:param base_url: The URL to make HTTP calls against
:type base_url: str
:param \\**client_kwargs: Arguments to pass to the connection initialization. These are"
"passed to a urllib3 `PoolManager <https://urllib3.readthedocs.io/en/latest/reference/"
"index.html?highlight=PoolManager#urllib3.PoolManager>`_. All arguments that can be passed to "
"a PoolManager are valid arguments."
"""
def __init__(self, base_url, cert_data, **client_kwargs):
self._conn = None
self.base_url = base_url
# Default values for connection properties
self._connection_properties = {
"timeout": urllib3.util.Timeout(connect=4800, read=4800),
"retries": urllib3.util.Retry(connect=50, read=50, redirect=50),
}
self._connection_properties.update(client_kwargs)
if cert_data:
if ("cert_file" in cert_data and cert_data["cert_file"]) or (
"ca_certs" in cert_data and cert_data["ca_certs"]
):
self._connection_properties.update({"ca_cert_data": cert_data})
self._proxy = self._connection_properties.pop("proxy", None)
self.session_key = self._connection_properties.pop("session_key", None)
self.session_location = self._connection_properties.pop("session_location", None)
self.log_dir = self._connection_properties.pop("log_dir", None)
self._init_connection()
@property
def proxy(self):
"""The proxy, if any."""
return self._proxy
@proxy.setter
def proxy(self, proxy):
"""set the proxy"""
self._proxy = proxy
def _init_connection(self):
"""Function for initiating connection with remote server"""
# For certificate-based authentication, we don't verify the server's certificate
# We're providing our client certificate to authenticate TO the server
cert_reqs = "CERT_NONE"
if self._connection_properties.get("ca_cert_data"):
LOGGER.info("Using certificate-based authentication.")
ca_cert_data = self._connection_properties.pop("ca_cert_data")
# Extract certificate files
cert_file = ca_cert_data.get("cert_file")
key_file = ca_cert_data.get("key_file")
ca_certs = ca_cert_data.get("ca_certs")
# Add client certificate and key for authentication
if cert_file:
self._connection_properties["cert_file"] = cert_file
if key_file:
self._connection_properties["key_file"] = key_file
# If CA certs provided, use them to verify server certificate
if ca_certs:
self._connection_properties["ca_certs"] = ca_certs
cert_reqs = "CERT_REQUIRED"
LOGGER.info("Server certificate verification enabled with provided CA.")
if self.proxy:
if self.proxy.startswith("socks"):
LOGGER.info("Initializing a SOCKS proxy.")
http = SOCKSProxyManager(self.proxy, cert_reqs=cert_reqs, maxsize=50, **self._connection_properties)
else:
LOGGER.info("Initializing a HTTP proxy.")
http = ProxyManager(self.proxy, cert_reqs=cert_reqs, maxsize=50, **self._connection_properties)
else:
LOGGER.info("Initializing no proxy.")
http = PoolManager(cert_reqs=cert_reqs, maxsize=50, **self._connection_properties)
self._conn = http.request
def rest_request(self, path, method="GET", args=None, body=None, headers=None):
"""Format and do HTTP Rest request
:param path: The URI path to perform the operation on.
:type path: str
:param method: method to perform on the path.
:type method: str
:param args: Any query to add to the URI. (Can also be directly added to the URI)
:type args: dict
:param body: body payload to include in the request if needed.
:type body: dict
:param headers: Any extra headers to add to the request.
:type headers: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
# TODO: Need to remove redfish.dmtf.org calls from here, add to their own HttpConnection
files = None
request_args = {}
if isinstance(path, bytes):
path = path.decode("utf-8")
external_uri = True if "redfish.dmtf.org" in path else False
else:
external_uri = True if "redfish.dmtf.org" in path else False
headers = {} if external_uri else headers
reqpath = path.replace("//", "/") if not external_uri else path
if body is not None:
if body and isinstance(body, list) and isinstance(body[0], tuple):
files = body
body = None
elif isinstance(body, (dict, list)):
headers["Content-Type"] = "application/json"
body = json.dumps(body)
elif not files:
headers["Content-Type"] = "application/octet-stream"
if method == "PUT":
resp = self.rest_request(method="HEAD", path=path, headers=headers)
try:
if resp.getheader("content-encoding") == "gzip":
buf = BytesIO()
gfile = gzip.GzipFile(mode="wb", fileobj=buf)
try:
gfile.write(str(body).encode("utf-8") if six.PY3 else str(body))
finally:
gfile.close()
compresseddata = buf.getvalue()
if compresseddata:
data = bytearray()
data.extend(memoryview(compresseddata))
body = data
except BaseException as excp:
LOGGER.error("Error occur while compressing body: %s", excp)
raise
if args:
if method == "GET":
reqpath += "?" + urlencode(args)
elif method == "PUT" or method == "POST" or method == "PATCH":
headers["Content-Type"] = "application/x-www-form-urlencoded"
body = urlencode(args)
# TODO: ADD to the default headers?
if headers is not None:
headers["Accept-Encoding"] = "gzip"
restreq = RestRequest(path, method, data=files if files else body, url=self.base_url)
if LOGGER.isEnabledFor(logging.DEBUG):
try:
logbody = None
if restreq.body:
if restreq.body[0] == "{":
logbody = restreq.body
else:
raise KeyError()
if restreq.method in ["POST", "PATCH"]:
debugjson = json.loads(restreq.body)
debugjson_masked = SecurityMasker.mask_simple_body(debugjson)
logbody = json.dumps(debugjson_masked)
logbody = logbody.replace("\\\\", "\\")
headers_masked = SecurityMasker.mask_http_headers(headers)
LOGGER.debug(
"HTTP REQUEST: %s\n\tPATH: %s\n\t" "HEADERS: %s\n\tBODY: %s",
restreq.method,
restreq.path,
headers_masked,
logbody,
)
except:
LOGGER.debug(
"HTTP REQUEST: %s\n\tPATH: %s\n\tBODY: %s",
restreq.method,
restreq.path,
"binary body",
)
inittime = time.time()
reqfullpath = self.base_url + reqpath if not external_uri else reqpath
# To ensure we don't have unicode/string merging issues in httplib of Python 2
if isinstance(reqfullpath, six.text_type):
reqfullpath = str(reqfullpath)
if headers:
request_args["headers"] = headers
if files:
request_args["fields"] = files
else:
request_args["body"] = body
try:
resp = self._conn(method, reqfullpath, **request_args)
except MaxRetryError:
vnic_url = "16.1.15.1"
if reqfullpath.find(vnic_url) != -1:
raise VnicNotEnabledError()
raise RetriesExhaustedError()
except DecodeError:
raise DecompressResponseError()
endtime = time.time()
LOGGER.info("Response Time to %s: %s seconds.", restreq.path, str(endtime - inittime))
restresp = RestResponse(restreq, resp)
# if restresp.request.body:
# respbody = restresp.read
# respbody = respbody.replace("\\\\", "\\")
if LOGGER.isEnabledFor(logging.DEBUG):
headerstr = ""
if restresp is not None:
respheader = restresp.getheaders()
for kiy, headerval in respheader.items():
headerstr += "\t" + kiy + ": " + headerval + "\n"
try:
headerstr_masked = SecurityMasker.mask_http_headers(headerstr)
response_body = SecurityMasker.mask_simple_body(restresp.read)
LOGGER.debug(
"HTTP RESPONSE for %s:\nCode:%s\nHeaders:" "\n%s\nBody Response of %s: %s",
restresp.request.path,
str(restresp._http_response.status) + " " + restresp._http_response.reason,
headerstr_masked,
restresp.request.path,
response_body,
)
except:
LOGGER.debug("HTTP RESPONSE:\nCode:%s", restresp)
else:
LOGGER.debug("HTTP RESPONSE: No HTTP Response obtained")
return restresp
def cert_login(self):
"""Login using a certificate."""
resp = self.rest_request("/html/login_cert.html", "GET")
if resp.status == 200 or resp.status == 201:
token = resp.getheader("X-Auth-Token")
location = resp.getheader("Location")
else:
raise InvalidCertificateError("")
return token, location
class Blobstore2Connection(object):
"""A connection for communicating locally with HPE servers
:param \\**conn_kwargs: Arguments to pass to the connection initialization.
Possible arguments for *\\**conn_kwargs* include:
:username: The username to login with
:password: The password to login with
"""
_http_vsn_str = "HTTP/1.1"
blobstore_headers = {"Accept": "*/*", "Connection": "Keep-Alive"}
def __del__(self):
"""Clear channel"""
self._conn = None
def __init__(self, **conn_kwargs):
self._conn = None
self.base_url = "blobstore://."
self._connection_properties = conn_kwargs
self.session_key = self._connection_properties.pop("sessionid", None)
self._init_connection(**self._connection_properties)
def _init_connection(self, **kwargs):
"""Initiate blobstore connection"""
# mixed security modes require a password at all times
username = kwargs.pop("username", "nousername")
if isinstance(username, bytes):
username = username.decode("utf-8")
password = kwargs.pop("password", "nopassword")
if isinstance(password, bytes):
password = password.decode("utf-8")
log_dir = kwargs.pop("log_dir", "")
try:
correctcreds = BlobStore2.initializecreds(username=username, password=password, log_dir=log_dir)
bs2 = BlobStore2()
if not correctcreds:
security_state = int(bs2.get_security_state())
if security_state == 3:
raise InvalidCredentialsError(0)
else:
raise SecurityStateError(security_state)
except Blob2SecurityError:
raise InvalidCredentialsError(0)
except HpIloChifPacketExchangeError as excp:
LOGGER.info("Exception: %s", str(excp))
raise ChifDriverMissingOrNotFound()
except Exception as excp:
if str(excp) == "chif":
raise ChifDriverMissingOrNotFound()
else:
raise
else:
self._conn = bs2
def rest_request(self, path="", method="GET", args=None, body=None, headers=None):
"""Rest request for blobstore client
:param path: The URI path to perform the operation on.
:type path: str
:param method: method to perform on the path.
:type method: str
:param args: Any query to add to the URI. (Can also be directly added to the URI)
:type args: dict
:param body: body payload to include in the request if needed.
:type body: dict
:param headers: Any extra headers to add to the request.
:type headers: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
# default headers if not passed in - otherwise will throw on .update call
if headers is None:
headers = {}
else:
headers.update(Blobstore2Connection.blobstore_headers)
if isinstance(path, bytes):
path = path.decode("utf-8")
reqpath = path.replace("//", "/")
oribody = body
if body is not None:
if isinstance(body, (dict, list)):
headers["Content-Type"] = "application/json"
if isinstance(body, bytes):
body = body.decode("utf-8")
body = json.dumps(body)
elif isinstance(body, bytes):
headers["Content-Type"] = "application/octet-stream"
body = bytearray(body)
else:
headers["Content-Type"] = "application/x-www-form-urlencoded"
body = urlencode(body)
if method == "PUT":
resp = self.rest_request(path=path, headers=headers)
try:
if resp.getheader("content-encoding") == "gzip":
buf = BytesIO()
gfile = gzip.GzipFile(mode="wb", fileobj=buf)
try:
gfile.write(str(body).encode("utf-8") if six.PY3 else str(body))
finally:
gfile.close()
compresseddata = buf.getvalue()
if compresseddata:
data = bytearray()
data.extend(memoryview(compresseddata))
body = data
except BaseException as excp:
LOGGER.error("Error occur while compressing body: %s", excp)
raise
headers["Content-Length"] = len(body)
if args:
if method == "GET":
reqpath += "?" + urlencode(args)
elif method == "PUT" or method == "POST" or method == "PATCH":
headers["Content-Type"] = "application/x-www-form-urlencoded"
body = urlencode(args)
str1 = "{} {} {}\r\n".format(method, reqpath, Blobstore2Connection._http_vsn_str)
str1 += "Host: \r\n"
str1 += "Accept-Encoding: gzip\r\n"
for header, value in headers.items():
str1 += "{}: {}\r\n".format(header, value)
str1 += "\r\n"
if body and len(body) > 0:
if isinstance(body, bytearray):
str1 = bytearray(str1.encode("utf-8")) + body
else:
# if isinstance(body, bytes):
# body = body.decode("utf-8")
str1 += body
if not isinstance(str1, bytearray):
str1 = bytearray(str1.encode("utf-8"))
if LOGGER.isEnabledFor(logging.DEBUG):
try:
logbody = None
if body:
if body[0] == "{":
logbody = body
else:
raise
if method in ["POST", "PATCH"]:
debugjson = json.loads(body)
debugjson_masked = SecurityMasker.mask_simple_body(debugjson)
logbody = json.dumps(debugjson_masked)
headers_masked = SecurityMasker.mask_http_headers(headers)
LOGGER.debug(
"Blobstore REQUEST: %s\n\tPATH: %s\n\tHEADERS: " "%s\n\tBODY: %s",
method,
str(headers_masked),
path,
logbody,
)
except:
LOGGER.debug(
"Blobstore REQUEST: %s\n\tPATH: %s\n\tHEADERS: " "%s\n\tBODY: %s",
method,
str(headers),
path,
"binary body",
)
inittime = time.time()
for idx in range(5):
try:
resp_txt = self._conn.rest_immediate(str1)
break
except Blob2OverrideError:
if idx == 4:
raise Blob2OverrideError(2)
else:
continue
endtime = time.time()
LOGGER.info("iLO Response Time to %s: %s secs.", path, str(endtime - inittime))
if resp_txt is not None:
# Dummy response to support a bad host response
if len(resp_txt) == 0:
resp_txt = (
"HTTP/1.1 500 Not Found\r\nAllow: "
"GET\r\nCache-Control: no-cache\r\nContent-length: "
"0\r\nContent-type: text/html\r\nDate: Tues, 1 Apr 2025 "
"00:00:01 GMT\r\nServer: "
"HP-iLO-Server/1.30\r\nX_HP-CHRP-Service-Version: 1.0.3\r\n\r\n\r\n"
)
restreq = RestRequest(path, method, data=body, url=self.base_url)
rest_response = RisRestResponse(restreq, resp_txt)
if rest_response.status in range(300, 399) and rest_response.status != 304:
newloc = rest_response.getheader("location")
newurl = urlparse(newloc)
rest_response = self.rest_request(newurl.path, method, args, oribody, headers)
try:
if rest_response.getheader("content-encoding") == "gzip":
if hasattr(gzip, "decompress"):
rest_response.read = gzip.decompress(rest_response.ori)
else:
compressedfile = BytesIO(rest_response.ori)
decompressedfile = gzip.GzipFile(fileobj=compressedfile)
rest_response.read = decompressedfile.read()
except Exception:
pass
if LOGGER.isEnabledFor(logging.DEBUG):
headerstr = ""
headerget = rest_response.getheaders()
for header in headerget:
headerstr += "\t" + header + ": " + headerget[header] + "\n"
try:
response_body = SecurityMasker.mask_complex_body(rest_response.read)
LOGGER.debug(
"Blobstore RESPONSE for %s:\nCode: %s\nHeaders:" "\n%s\nBody of %s: %s",
rest_response.request.path,
str(rest_response._http_response.status) + " " + rest_response._http_response.reason,
headerstr,
rest_response.request.path,
response_body,
)
except:
LOGGER.debug(
"Blobstore RESPONSE for %s:\nCode:%s",
rest_response.request.path,
rest_response,
)
return rest_response
def cert_login(self):
"""Login using a certificate."""
# local cert login is only available on iLO 5
token = self.cert_login()
resp = self.rest_request("/redfish/v1/SessionService/Sessions/", "GET")
if resp.status == 200:
try:
location = resp.obj.Oem.Hpe.Links.MySession["@odata.id"]
except KeyError:
raise InvalidCertificateError("")
else:
raise InvalidCertificateError("")
return token, location
@@ -0,0 +1,376 @@
###
# 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 -*-
"""Containers used for REST requests and responses."""
import json
import sys
try:
from collections import OrderedDict
except ImportError:
from collections.abc import OrderedDict
from six import BytesIO, StringIO, string_types, text_type
from six.moves import http_client
from urllib3.util import Timeout, Retry
class JSONEncoder(json.JSONEncoder):
"""JSON Encoder class"""
def default(self, obj):
"""Set defaults in JSON encoder class
:param obj: object to be encoded into JSON.
:type obj: RestResponse
:returns: A JSON :class:`OrderedDict`
"""
if isinstance(obj, RestResponse):
jsondict = OrderedDict()
jsondict["Status"] = obj.status
jsondict["Headers"] = obj.getheaders()
if obj.read:
jsondict["Content"] = obj.dict
return jsondict
if isinstance(obj, bytes):
obj = obj.decode("utf-8")
if isinstance(obj, Timeout):
jsondict = OrderedDict()
jsondict["connect_timeout"] = obj.connect_timeout
jsondict["read_timeout"] = obj.read_timeout
return jsondict
if isinstance(obj, Retry):
jsondict = OrderedDict()
jsondict["connect"] = obj.connect
jsondict["read"] = obj.read
jsondict["redirect"] = obj.redirect
return jsondict
return json.JSONEncoder.default(self, obj)
class JSONDecoder(json.JSONDecoder):
"""Custom JSONDecoder that understands our types"""
def decode(self, json_string):
"""Decode JSON string
:param json_string: The JSON string to be decoded into usable data.
:type json_string: str
:returns: returns a parsed dict
"""
parsed_dict = super(JSONDecoder, self).decode(json_string)
return parsed_dict
class _FakeSocket(BytesIO):
"""slick way to parse a http response.
http://pythonwise.blogspot.com/2010/02/parse-http-response.html"""
def makefile(self, *args, **kwargs):
"""Return self object"""
return self
class RisObject(dict):
"""Converts a JSON/Rest dict into a object so you can use .property notation
:param d: dictionary to be converted
:type d: dict
"""
__getattr__ = dict.__getitem__
def __init__(self, d):
"""Initialize RisObject"""
super(RisObject, self).__init__()
self.update(**dict((k, self.parse(value)) for k, value in list(d.items())))
@classmethod
def parse(cls, value):
"""Parse for RIS value
:param cls: class referenced from class method
:type cls: RisObject
:param value: value to be parsed
:type value: data type
:returns: returns parsed value
"""
if isinstance(value, dict):
return cls(value)
elif isinstance(value, list):
return [cls.parse(i) for i in value]
return value
class RestRequest(object):
"""Holder for Request information
:param path: The URI path.
:type path: str
:param method: method to be implemented
:type method: str
:param data: body payload for the rest call
:type data: dict
"""
def __init__(self, path, method="GET", data="", url=None):
self._path = path
self._body = data
self._method = method
self.url = url
@property
def path(self):
"""The path the request is made against."""
return self._path
@property
def method(self):
"""The method to implement."""
return self._method
@property
def body(self):
"""The body to pass along with the request, if any."""
return self._body
def __str__(self):
"""Format string"""
body = "" if not self._body else self._body
try:
return "{} {}\n\n{}".format(self.method, self.path, body)
except:
return "{} {}\n\n{}".format(self.method, self.path, "")
class RestResponse(object):
"""Returned by Rest requests
:param rest_request: Holder for request information
:type rest_request: :class:`RestRequest` object
:param http_response: Response from HTTP
:type http_response: :class:`HTTPResponse` object
"""
def __init__(self, rest_request, http_response):
self._read = None
self._status = None
self._headers = None
self._session_key = None
self._session_location = None
self._rest_request = rest_request
self._http_response = http_response
self._read = self._http_response.data if http_response is not None else None
self._ori = self._read
@property
def read(self):
"""The response body, attempted to be translated into json, else is a string."""
if self._read and not isinstance(self._read, text_type):
self._read = self._read.decode("utf-8", "ignore")
return self._read
@read.setter
def read(self, read):
"""Property for setting _read
:param read: The data to set to read.
:type read: str
"""
if read is not None:
if isinstance(read, dict):
read = json.dumps(read, indent=4)
self._read = read
def getheaders(self):
"""Get all headers included in the response."""
return dict(self._http_response.headers) if self._http_response is not None else self._headers
def getheader(self, name):
"""Case-insensitive search for an individual header
:param name: The header name to retrieve.
:type name: str
:returns: returns a header from HTTP response or None if not found.
"""
def search_dict(search_key, dct):
for key, val in dct.items():
if key.lower() == search_key.lower():
return val
return None
if self._http_response:
return search_dict(name, self._http_response.headers)
return search_dict(name, self._headers)
def loaddict(self, newdict):
"""Property for setting JSON data. Used during initialization.
:param newdict: The string data to set as JSON data.
:type newdict: str
"""
self._read = json.dumps(newdict, indent=4)
@property
def dict(self):
"""The response body data as an dict"""
try:
return json.loads(self.read)
except ValueError as exp:
if (
self.path != "/smbios"
and self.path != "/cgi-bin/uploadFile"
and "/redfish/v1/Systems/1/Storage/" not in self.path
):
sys.stderr.write("An invalid response body was returned: %s" % exp)
return None
@property
def obj(self):
"""The response body data as an object"""
return RisObject.parse(self.dict)
@property
def ori(self):
"""The original response body data"""
return self._ori
@property
def status(self):
"""The status code of the request."""
if self._status:
return self._status
return self._http_response.status if self._http_response is not None else self._status
@property
def session_key(self):
"""The saved session key for the connection."""
if self._session_key:
return self._session_key
self._session_key = self.getheader("x-auth-token")
return self._session_key
@property
def session_location(self):
"""The saved session location, used for logging out."""
if self._session_location:
return self._session_location
self._session_location = self.getheader("location")
return self._session_location
@property
def request(self):
"""The saved http request the response was generated by."""
return self._rest_request
@property
def path(self):
"""The path the request was made against."""
return self.request.path
def __str__(self):
"""Class string formatter"""
headerstr = ""
for kiy, val in self.getheaders().items():
headerstr += "%s %s\n" % (kiy, val)
return "%(status)s\n%(headerstr)s\n\n%(body)s" % {
"status": self.status,
"headerstr": headerstr,
"body": self.read,
}
class RisRestResponse(RestResponse):
"""Returned by Rest requests from CHIF
:param rest_request: Holder for request information
:type rest_request: :class:`RestRequest` object
:param resp_text: text from response to be buffered and read
:type resp_text: str
"""
def __init__(self, rest_request, resp_txt):
"""Initialization of RisRestResponse"""
if not isinstance(resp_txt, string_types):
resp_txt = "".join(map(chr, resp_txt))
self._respfh = StringIO(resp_txt)
self._socket = _FakeSocket(bytearray(list(map(ord, self._respfh.read()))))
# self._respfh = BytesIO(resp_txt.encode('utf-8'))
# self._socket = _FakeSocket(bytearray(self._respfh.read()))
response = http_client.HTTPResponse(self._socket)
# response = response.decode('utf-8')
response.begin()
response.data = response.read()
response.headers = {ki[0]: ki[1] for ki in response.getheaders()}
super(RisRestResponse, self).__init__(rest_request, response)
class StaticRestResponse(RestResponse):
"""A RestResponse object used when data is being cached."""
def __init__(self, **kwargs):
restreq = None
if "restreq" in kwargs:
restreq = kwargs["restreq"]
super(StaticRestResponse, self).__init__(restreq, None)
if "Status" in kwargs:
self._status = kwargs["Status"]
if "Headers" in kwargs:
self._headers = kwargs["Headers"]
if "session_key" in kwargs:
self._session_key = kwargs["session_key"]
if "session_location" in kwargs:
self._session_location = kwargs["session_location"]
if "Content" in kwargs:
content = kwargs["Content"]
if isinstance(content, string_types):
self._read = content
else:
self._read = json.dumps(content)
else:
self._read = ""
def getheaders(self):
"""Function for accessing the headers"""
returnlist = {}
if isinstance(self._headers, dict):
returnlist = self._headers
elif isinstance(self._headers, (list, tuple)):
returnlist = {ki[0]: ki[1] for ki in self._headers}
else:
for item in self._headers:
returnlist.update(item.items()[0])
return returnlist
@@ -0,0 +1,643 @@
###
# 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 -*-
"""Direct module for working with Redfish/REST technology."""
# ---------Imports---------
import base64
import hashlib
import logging
import sys
from six.moves.urllib.parse import urlparse
from redfish.rest.connections import (
Blobstore2Connection,
HttpConnection,
InvalidCredentialsError,
OneTimePasscodeError,
UnauthorizedLoginAttemptError,
TokenExpiredError,
)
# ---------End of imports---------
# ---------Debug logger---------
LOGGER = logging.getLogger(__name__)
# ---------End of debug logger---------
class ServerDownOrUnreachableError(Exception):
"""Raised when server is unreachable."""
pass
class JsonDecodingError(Exception):
"""Raised when there is an error in json data."""
pass
class AuthMethod(object):
"""AUTH Method class
**BASIC**, **SESSION**, **CERTIFICATE** variables translate to their string counterparts
`basic`, `session`, `certificate`."""
BASIC = "basic"
SESSION = "session"
CERTIFICATE = "certificate"
class RestClientBase(object):
"""Base REST client. Each RestClientBase has a connection object built by parsing the
client_kwargs. This connection is used for communicating remotely or locally.
:param biospassword: The iLO Gen9 bios password. See :func:`bios_password`
:type biospassword: str
:param \\**client_kwargs: Arguments to pass to the client argument. For possible values see
:mod:`redfish.rest.connections.Blobstore2Connection` for a local
connection or :mod:`redfish.rest.connections.HttpConnection`
for remote connection."""
def __init__(self, biospassword=None, **client_kwargs):
self.connection = None
self._biospassword = biospassword
self._build_connection(**client_kwargs)
@property
def bios_password(self):
"""Property for the biospassword. Only required on Gen9 iLO 4 when RBSU bios password is set
and modifying bios settings
"""
return self._biospassword
@bios_password.setter
def bios_password(self, bios_pw):
"""set the bios password"""
self._biospassword = bios_pw
def _build_connection(self, **conn_kwargs):
"""Build the appropriate connection for the client"""
base_url = conn_kwargs.pop("base_url", None)
if not base_url or base_url.startswith("blobstore://"):
self.connection = Blobstore2Connection(**conn_kwargs)
elif not base_url.startswith("https://"):
base_url = "https://" + base_url
_ = conn_kwargs.pop("username", None)
_ = conn_kwargs.pop("password", None)
_ = conn_kwargs.pop("sessionid", None)
_ = conn_kwargs.pop("login_otp", None)
self.connection = HttpConnection(base_url, self._cert_data, **conn_kwargs)
else:
_ = conn_kwargs.pop("username", None)
_ = conn_kwargs.pop("password", None)
_ = conn_kwargs.pop("sessionid", None)
_ = conn_kwargs.pop("login_otp", None)
self.connection = HttpConnection(base_url, self._cert_data, **conn_kwargs)
def _get_req_headers(self, headers=None):
"""Base _get_req_headers function"""
return headers if headers else {}
def get(self, path, args=None, headers=None, noauth=False):
"""Perform a GET request
:param path: The URI path.
:type path: str
:param args: Any query to add to the URI. (Can also be directly added to the URI)
:type args: dict
:param headers: Any extra headers to add to the request.
:type headers: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
try:
return self.connection.rest_request(
path,
method="GET",
args=args,
headers=self._get_req_headers(headers=headers, noauth=noauth),
)
except ValueError:
LOGGER.debug("Error in json object getting path: %s", path)
raise JsonDecodingError("Error in json decoding.")
def patch(self, path, body, args=None, headers=None, noauth=False):
"""Perform a PATCH request
:param path: The URI path.
:type path: str
:param body: The body to pass with the request.
:type body: dict
:param args: Any query to add to the URI. (Can also be directly added to the URI)
:type args: dict
:param headers: Any extra headers to add to the request.
:type headers: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
return self.connection.rest_request(
path,
body=body,
method="PATCH",
args=args,
headers=self._get_req_headers(headers=headers, noauth=noauth),
)
def post(self, path, body, args=None, headers=None, noauth=False):
"""Perform a POST request
:param path: The URI path.
:type path: str
:param body: The body to pass with the request.
:type body: dict
:param args: Any query to add to the URI. (Can also be directly added to the URI)
:type args: dict
:param headers: Any extra headers to add to the request.
:type headers: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
return self.connection.rest_request(
path,
body=body,
method="POST",
args=args,
headers=self._get_req_headers(headers=headers, noauth=noauth),
)
def put(self, path, body, args=None, headers=None, noauth=False):
"""Perform a PUT request
:param path: The URI path.
:type path: str
:param body: The body to pass with the request.
:type body: dict
:param args: Any query to add to the URI. (Can also be directly added to the URI)
:type args: dict
:param headers: Any extra headers to add to the request.
:type headers: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
return self.connection.rest_request(
path,
body=body,
method="PUT",
args=args,
headers=self._get_req_headers(headers=headers, noauth=noauth),
)
def head(self, path, headers=None):
"""Perform a HEAD request
:param path: The URI path.
:type path: str
:param headers: Any extra headers to add to the request.
:type headers: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
return self.connection.rest_request(path, method="HEAD", headers=self._get_req_headers(headers=headers))
def delete(self, path, headers=None):
"""Perform a DELETE request
:param path: The URI path.
:type path: str
:param args: Any query to add to the URI. (Can also be directly added to the URI)
:type args: dict
:returns: A :class:`redfish.rest.containers.RestResponse` object
"""
return self.connection.rest_request(path, method="DELETE", headers=self._get_req_headers(headers=headers))
class RestClient(RestClientBase):
"""REST client with Redfish and LegacyRest support built on top.
:param default_prefix: The root of the REST API, either /redfish/v1/ or /rest/v1.
:type default_prefix: str
:param is_redfish: Flag to force redfish conformance, even on LegacyRest clients.
:type is_redfish: bool
:param username: The username of the account to login with.
:type username: str
:param password: The password of the account to login with.
:type password: str
:param auth: The authentication type to force.
:type auth: str or :class:`AuthMethod` class variable.
:param ca_cert_data: Dictionary containing the certificate authority data (user CA, \
user root CA, user root CA key
:type ca_cert_data: dict
:param \\**client_kwargs: Arguments to create a :class:`RestClientBase` instance.
"""
def __init__(
self,
default_prefix="/redfish/v1/",
is_redfish=True,
username=None,
password=None,
sessionid=None,
base_url=None,
auth=None,
ca_cert_data=None,
login_otp=None,
session_location=None,
**client_kwargs
):
"""Create a Rest Client object"""
self.default_prefix = default_prefix
self.is_redfish = is_redfish
self.root = None
self.auth_type = self._get_auth_type(auth, ca_cert_data=ca_cert_data, **client_kwargs)
self._auth_key = None
self._user_pass = (username, password)
self._session_location = None
self._cert_data = ca_cert_data
self.login_otp = login_otp
super(RestClient, self).__init__(
username=username,
password=password,
sessionid=sessionid,
base_url=base_url,
login_otp=login_otp,
session_location=session_location,
**client_kwargs
)
def __enter__(self):
"""Create a connection and return the session object"""
self.login()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Close the connection"""
self.logout()
def _get_auth_type(self, auth_param, ca_cert_data=None, **client_kwargs):
"""Get the auth type based on key args or positional argument.
Defaults to session auth."""
if not auth_param:
# _ca_cert_data = client_kwargs.get('ca_cert_data')
if ca_cert_data:
if ("ca_certs" in ca_cert_data and ca_cert_data["ca_certs"]) or (
"cert_file" in ca_cert_data and ca_cert_data["cert_file"]
):
if (ca_cert_data.get("cert_file") and ca_cert_data.get("key_file")) or ca_cert_data.get("ca_certs"):
return AuthMethod.CERTIFICATE
return AuthMethod.SESSION
return auth_param
@property
def base_url(self):
"""The connection's URL to make calls against"""
return self.connection.base_url
@property
def proxy(self):
"""The connection's proxy, if any."""
try:
proxy = self.connection.proxy
except AttributeError:
proxy = None
return proxy
@property
def session_key(self):
"""The Client's session key, if any."""
return self._auth_key if self.auth_type in [AuthMethod.SESSION, AuthMethod.CERTIFICATE] else None
@session_key.setter
def session_key(self, ses_key):
"""Set _auth_key to a session key"""
self._auth_key = ses_key
@property
def basic_auth(self):
"""The Client's basic auth header, if any."""
return self._auth_key if self.auth_type == AuthMethod.BASIC else None
@basic_auth.setter
def basic_auth(self, bas_key):
"""Set _auth_key to a basic auth header"""
self._auth_key = bas_key
@property
def session_location(self):
"""The session URI. Used for deleting the session when we logout."""
session_loc = None
if self._session_location:
if self.base_url == "blobstore://.":
session_loc = self._session_location.replace("https://", "")
session_loc = session_loc.replace(" ", "%20")
else:
parse_object = urlparse(self.base_url)
if parse_object.hostname is not None:
newurl = "https://" + parse_object.hostname
session_loc = self._session_location.replace(newurl, "")
else:
session_loc = self._session_location.replace(self.base_url, "")
return session_loc
@session_location.setter
def session_location(self, ses_loc):
"""Set the session URI"""
login_url_p = urlparse(self.base_url)
new_port = login_url_p.port
# The session_url returned by iLO does not include the port information
# If we're using a non-standard port to connect to iLO we have to
# manually insert it, based on the base_url.
if new_port and ses_loc:
session_location_p = urlparse(ses_loc)
session_location_p = session_location_p._replace(
netloc="{}:{}".format(session_location_p.hostname, new_port)
)
self._session_location = session_location_p.geturl()
else:
self._session_location = ses_loc
@property
def username(self):
"""The username, if any. Once a login function has been called the credentials are removed
from memory for security and this will return None."""
return self._user_pass[0]
@property
def password(self):
"""The password, if any. Once a login function has been called the credentials are removed
from memory for security and this will return None."""
return self._user_pass[1]
@property
def login_url(self):
"""The login URI from the root response. This is where we post the
credentials for a login."""
login_url = None
try:
login_url = self.root.obj.Links.Sessions["@odata.id"]
except KeyError:
login_url = self.root.obj.links.Sessions.href
finally:
if not login_url:
raise ServerDownOrUnreachableError("Cannot locate the login url. Is this a Rest or" " Redfish server?")
return login_url
def login(self, auth=AuthMethod.SESSION):
"""Login to a Redfish or LegacyRest server.
If auth is not supplied login will intelligently
choose the authentication mode based on the arguments passed.
Basic authentication MUST be specified with `auth`.
:param auth: The auth type to login with.
:type auth: str or :class:`AuthMethod` class variable
"""
if auth:
auth = self._get_auth_type(auth)
self.auth_type = auth
if self.auth_type is AuthMethod.BASIC:
self._get_root()
self._basic_login()
elif self.auth_type is AuthMethod.SESSION:
self._get_root()
self._session_login()
elif self.auth_type is AuthMethod.CERTIFICATE:
self._cert_login()
self._get_root()
def logout(self):
"""Logout of session.
YOU MUST CALL THIS WHEN YOU ARE DONE TO FREE UP SESSIONS"""
if self.session_location:
resp = self.delete(self.session_location)
LOGGER.info("User logged out: %s", resp.read)
self._auth_key = None
self.session_location = None
def _get_root(self):
"""Get the root response of the server"""
if not self.root:
resp = self.get(self.default_prefix)
if resp.status != 200:
raise ServerDownOrUnreachableError("Server not reachable, " "return code: %d" % resp.status)
self.root = resp
def _basic_login(self):
"""Login using basic authentication"""
LOGGER.info("Performing basic authentication.")
if not self.basic_auth:
auth_key = base64.b64encode(("{}:{}".format(self.username, self.password)).encode("utf-8")).decode("utf-8")
self.basic_auth = "Basic {}".format(auth_key)
headers = dict()
headers["Authorization"] = self.basic_auth
respvalidate = self.get(self.login_url, headers=headers)
if respvalidate.status == 401:
self._credential_err()
else:
self._user_pass = (None, None)
def _session_login(self):
"""Login using session authentication"""
if not self.connection.session_key:
LOGGER.info("Performing session authentication.")
data = dict()
data["UserName"] = self.username
data["Password"] = self.password
if self.login_otp is not None:
data["Token"] = self.login_otp
headers = dict()
resp = self.post(self.login_url, body=data, headers=headers)
try:
respread = resp.read
respread = respread.replace("\\\\", "\\")
# LOGGER.info("%s" % respread)
except ValueError:
pass
LOGGER.debug("Login returned code %s: %s", resp.status, respread)
self.session_key = resp.session_key
self.session_location = resp.session_location
self.login_return_code = resp.status
self.login_response = respread
else:
self.session_key = self.connection.session_key
if self.connection.session_location:
self.session_location = self.connection.session_location
if hasattr(self, "login_response") and self.login_response:
if "OneTimePasscodeSent" in self.login_response:
raise OneTimePasscodeError()
elif "UnauthorizedLogin" in self.login_response:
if self.login_otp:
raise UnauthorizedLoginAttemptError(
"Error " + str(self.login_return_code) + ". Login is unauthorized.\n"
"Please check the credentials/OTP entered.\n"
)
else:
self._credential_err()
elif "TokenExpired" in self.login_response:
raise TokenExpiredError(
"Error "
+ str(self.login_return_code)
+ ". The OTP entered has expired. Please enter credentials again.\n"
)
elif not self.session_key and not resp.status == 200:
self._credential_err()
else:
self._user_pass = (None, None)
def _cert_login(self):
"""Login using certificate authentication"""
self.session_key, self.session_location = self.connection.cert_login()
def _credential_err(self):
"""Return credential error based on delay"""
try:
if self.is_redfish:
delay = self.root.obj.Oem.Hpe.Sessions.LoginFailureDelay
else:
delay = self.root.obj.Oem.Hp.Sessions.LoginFailureDelay
except KeyError:
delay = 5
raise InvalidCredentialsError(delay)
def _get_req_headers(self, headers=None, optionalpassword=None, noauth=False):
"""Get the request headers
:param headers: additional headers to be utilized
:type headers: str
:param optionalpassword: provide password for authentication.
:type optionalpassword: str.
:returns: returns headers
"""
headers = headers if isinstance(headers, dict) else dict()
h_list = [header.lower() for header in headers]
auth_headers = True if "x-auth-token" in h_list or "authorization" in h_list else False
token = self._biospassword if self._biospassword else optionalpassword
if token:
token = optionalpassword.encode("utf-8") if type(optionalpassword).__name__ in "basestr" else token
hash_object = hashlib.new("SHA256")
hash_object.update(token)
if not noauth:
headers["X-HPRESTFULAPI-AuthToken"] = hash_object.hexdigest().upper()
if self.session_key and not auth_headers:
if not noauth:
headers["X-Auth-Token"] = self.session_key
elif self.basic_auth and not auth_headers:
if not noauth:
headers["Authorization"] = self.basic_auth
if self.is_redfish:
if not noauth:
headers["OData-Version"] = "4.0"
return headers
def get_resource_directory(self):
try:
resource_uri = self.root.obj.Oem.Hpe.Links.ResourceDirectory["@odata.id"]
except KeyError:
sys.stderr.write("Resource directory is only available on HPE servers.\n")
return None
response = self.get(resource_uri)
resources = []
if response.status == 200:
sys.stdout.write("\tFound resource directory at /redfish/v1/resourcedirectory" + "\n\n")
resources = response.dict["Instances"]
else:
sys.stderr.write("\tResource directory missing at /redfish/v1/resourcedirectory" + "\n")
return resources
def get_gen(self):
rootresp = self.root.obj
# Default iLO 5
ilogen = 5
iloversion = None
gencompany = next(iter(rootresp.get("Oem", {}).keys()), None) in ("Hpe", "Hp")
comp = "Hp" if 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):
ilogen = next(iter(rootresp.get("Oem", {}).get(comp, {}).get("Manager", {}))).get("ManagerType")
ilover = next(iter(rootresp.get("Oem", {}).get(comp, {}).get("Manager", {}))).get("ManagerFirmwareVersion")
if ilogen.split(" ")[-1] == "CM":
# Assume iLO 4 types in Moonshot
ilogen = 4
iloversion = None
else:
ilogen = ilogen.split(" ")[1]
iloversion = float(ilogen.split(" ")[-1] + "." + "".join(ilover.split(".")))
return (ilogen, iloversion)
class LegacyRestClient(RestClient):
"""Class for a **Legacy REST** client instance.
Instantiates appropriate Rest object based on existing configuration.
Use this to retrieve a pre-configured Legacy Rest Class.
Basic arguments include (These can be omitted for most local connections):
* **base_url**: The IP or Hostname of the server to perform operations on.
* **username**: The username of the account to login with.
* **password**: The username of the account to login with.
For full description of the arguments allowed see :class:`RestClient`"""
def __init__(self, **client_kwargs):
kwargs = {
"default_prefix": client_kwargs.pop("default_prefix", "/rest/v1"),
"is_redfish": client_kwargs.pop("is_redfish", False),
}
kwargs.update(client_kwargs)
super(LegacyRestClient, self).__init__(**kwargs)
class RedfishClient(RestClient):
"""Class for a **Redfish** client instance.
Instantiates appropriate Redfish object based on existing configuration.
Use this to retrieve a pre-configured Redfish Class.
Basic arguments include (These can be omitted for most local connections):
* **base_url**: The IP or Hostname of the server to perform operations on. None for local.
* **username**: The username of the account to login with.
* **password**: The username of the account to login with.
For full description of the arguments allowed see :class:`RestClient`"""
def __init__(self, **client_kwargs):
kwargs = {
"default_prefix": client_kwargs.pop("default_prefix", "/redfish/v1"),
"is_redfish": client_kwargs.pop("is_redfish", True),
}
kwargs.update(client_kwargs)
super(RedfishClient, self).__init__(**kwargs)
@@ -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
@@ -0,0 +1,678 @@
###
# Copyright 2025 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 -*-
"""Centralized security masking framework for iLORest"""
import json
import re
import copy
from typing import Dict, List, Any, Union, Optional
class SecurityMasker:
"""Centralized framework for masking sensitive information in logs and outputs."""
# Default mask character
MASK_VALUE = "******"
# Number of characters to show for partial masking
PARTIAL_MASK_SHOW_CHARS = 4
# Sensitive command-line argument patterns
SENSITIVE_CLI_KEYWORDS = {
"--password",
"--pass",
"-p",
"--pwd",
"--token",
"--auth-token",
"--session-token",
"--key",
"--api-key",
"--secret",
"--credential",
"--cert-key",
"--private-key",
"--passphrase",
"--sessionkey",
"--auth",
"--authorization",
"--filepass",
"--backup-password",
"--biospassword",
}
# Fields requiring COMPLETE masking (no characters visible)
COMPLETE_MASK_FIELDS = {
"password",
"oldpassword",
"newpassword",
"currentpassword",
"passwd",
"pwd",
"pass",
"passphrase",
"secret",
"client_secret",
"private_key",
"privatekey",
"cert_key",
"certkey",
"pin",
"otp",
"totp",
"mfa_token",
"csrf_token",
"csrftoken",
}
# Fields allowing PARTIAL masking (last 4-6 chars visible for debugging)
PARTIAL_MASK_FIELDS = {
"token",
"auth_token",
"authtoken",
"session_token",
"sessiontoken",
"access_token",
"accesstoken",
"refresh_token",
"refreshtoken",
"bearer_token",
"api_key",
"apikey",
"sessionkey",
"session_key",
"credential",
"credentials",
"appid",
"app_id",
"applicationid",
"application_id",
}
# Headers requiring COMPLETE masking
COMPLETE_MASK_HEADERS = {
"authorization", # Often contains Basic auth with password
"www-authenticate",
"proxy-authorization",
"cookie",
"set-cookie", # May contain sensitive session data
}
# Headers allowing PARTIAL masking (for debugging/correlation)
PARTIAL_MASK_HEADERS = {
"x-auth-token",
"x-session-token",
"x-api-key",
"api-key",
"bearer", # When separate from Authorization header
"x-session-id",
"session-id",
}
# All sensitive headers (union of complete and partial)
SENSITIVE_HEADERS = (
COMPLETE_MASK_HEADERS
| PARTIAL_MASK_HEADERS
| {"authentication", "basic", "digest", "oauth", "jwt", "x-csrf-token", "x-request-id"}
)
# All sensitive body fields (union of complete and partial)
SENSITIVE_BODY_FIELDS = COMPLETE_MASK_FIELDS | PARTIAL_MASK_FIELDS | {"auth", "authorization"}
# URL parameter patterns that might contain sensitive data
SENSITIVE_URL_PARAMS = {"password", "token", "key", "secret", "auth", "session", "credential", "pass", "pwd"}
@classmethod
def mask_command_arguments(cls, args: List[str]) -> List[str]:
"""
Mask sensitive information in command-line arguments.
Args:
args: List of command-line arguments
Returns:
List of arguments with sensitive values masked
Example:
>>> SecurityMasker.mask_command_arguments(['--user', 'admin', '--password', 'MyP@ss'])
['--user', 'admin', '--password', '******']
"""
if not args:
return args
masked_args = []
i = 0
while i < len(args):
arg = args[i]
# Check for inline sensitive arguments (--password=value)
if cls._has_inline_sensitive_value(arg):
masked_args.append(cls._mask_inline_argument(arg))
i += 1
continue
# Check if current arg is a sensitive flag
if cls._is_sensitive_cli_flag(arg):
masked_args.append(arg) # Keep the flag
# Mask the next argument if it exists and isn't another flag
if i + 1 < len(args) and not args[i + 1].startswith("-"):
i += 1 # Move to the next argument
masked_args.append(cls.MASK_VALUE) # Mask the value
i += 1
continue
# Keep non-sensitive arguments as-is
masked_args.append(arg)
i += 1
return masked_args
@classmethod
def mask_http_headers(
cls, headers: Union[Dict[str, str], List[tuple], Any]
) -> Union[Dict[str, str], List[tuple], str]:
"""
Mask sensitive information in HTTP headers.
Args:
headers: HTTP headers in various formats (dict, list of tuples, or other)
Returns:
Headers with sensitive values masked
Examples:
# dict format
>>> SecurityMasker.mask_http_headers({'Authorization': 'Basic abc123', 'Accept': 'application/json'})
{'Authorization': '******', 'Accept': 'application/json'}
# list-of-tuples format
>>> SecurityMasker.mask_http_headers([('Authorization', 'Bearer abcd'), ('Host', 'example')])
[('Authorization', '******'), ('Host', 'example')]
"""
if not headers:
return headers
try:
# Handle dictionary format
if isinstance(headers, dict):
return cls._mask_headers_dict(headers)
# Handle list of tuples format
elif isinstance(headers, (list, tuple)):
return cls._mask_headers_list(headers)
# Handle string representation of headers
elif isinstance(headers, str):
return cls._mask_headers_string(headers)
# Handle other object types (e.g., HTTPMessage)
else:
return cls._mask_headers_object(headers)
except Exception:
# If masking fails, return a safe placeholder
return "[HEADERS MASKED DUE TO PROCESSING ERROR]"
@classmethod
def mask_simple_body(cls, body: Union[str, bytes, Dict[str, Any]]) -> Union[str, bytes, Dict[str, Any]]:
"""
Mask sensitive information in simple HTTP request/response bodies.
This method handles basic body masking for simple payloads.
For complex bodies with advanced JSON parsing and error handling,
use mask_complex_body() instead.
Args:
body: Request/response body in various formats
Returns:
Body with sensitive values masked
Examples:
# JSON string
>>> SecurityMasker.mask_simple_body('{"password":"secret"}')
'{"password":"******"}'
# dict
>>> SecurityMasker.mask_simple_body({"token": "abcd"})
{"token": "******"}
"""
if not body:
return body
try:
# Handle string body (likely JSON)
if isinstance(body, str):
return cls._mask_string_body(body)
# Handle bytes body
elif isinstance(body, bytes):
return cls._mask_bytes_body(body)
# Handle dictionary body
elif isinstance(body, dict):
return cls._mask_dict_body(body)
# Handle other types
else:
return body
except Exception:
# If masking fails, return original or safe placeholder
if isinstance(body, (str, bytes)):
return "[BODY MASKED DUE TO PROCESSING ERROR]"
return body
@classmethod
def mask_url_parameters(cls, url: str) -> str:
"""
Mask sensitive information in URL parameters.
Args:
url: URL string that may contain parameters
Returns:
URL with sensitive parameter values masked
Example:
>>> SecurityMasker.mask_url_parameters('https://api.example.com/login?user=admin&password=1234')
'https://api.example.com/login?user=admin&password=******'
"""
if not url or "?" not in url:
return url
try:
base_url, params = url.split("?", 1)
param_pairs = params.split("&")
masked_pairs = []
for pair in param_pairs:
if "=" in pair:
key, value = pair.split("=", 1)
if any(sensitive in key.lower() for sensitive in cls.SENSITIVE_URL_PARAMS):
masked_pairs.append(f"{key}={cls.MASK_VALUE}")
else:
masked_pairs.append(pair)
else:
masked_pairs.append(pair)
return f"{base_url}?{'&'.join(masked_pairs)}"
except Exception:
return url
@classmethod
def mask_complex_body(cls, body: Union[str, bytes, Dict[str, Any]]) -> Union[str, bytes]:
"""
Mask sensitive information in complex HTTP request/response bodies for logging.
This is a specialized method for complex body masking that handles:
- Advanced JSON parsing with error recovery
- Automatic encoding/decoding (str ↔ bytes)
- Type preservation (returns same type as input)
- Graceful error handling to ensure logging never fails
Use this method for complex request/response bodies. For simple bodies,
mask_simple_body() may be sufficient.
Args:
body: The request/response body in various formats (str, bytes, dict)
Returns:
Masked body as string or bytes (matching input type)
Example:
>>> SecurityMasker.mask_complex_body(b'{"users":[{"name":"u","password":"p"}]}')
b'{"users": [{"name": "u", "password": "******"}]}'
"""
if not body:
return body
original_type = type(body)
try:
# Convert to string if bytes
if isinstance(body, bytes):
try:
body = body.decode("utf-8")
except UnicodeDecodeError:
# If decode fails, return original bytes
return body
# If already a dict, mask and return as JSON string
if isinstance(body, dict):
masked_dict = cls._mask_dict_body(body)
return json.dumps(masked_dict)
# If string, check if it's JSON
if isinstance(body, str):
body = body.strip()
if body.startswith(("{", "[")):
try:
# Parse JSON and mask
body_json = json.loads(body)
masked_json = cls._mask_dict_body(body_json)
masked_string = json.dumps(masked_json)
# Return in original type
if original_type == bytes:
return masked_string.encode("utf-8")
return masked_string
except json.JSONDecodeError:
# Not valid JSON, return as-is
pass
# Return body as-is if not JSON or other type
if original_type == bytes and isinstance(body, str):
return body.encode("utf-8")
return body
except Exception:
# If anything fails, return original to ensure logging continues
# Converting to string if needed for logging
if isinstance(body, bytes):
try:
return body.decode("utf-8", errors="replace")
except:
return "[BODY - BINARY DATA]"
return body if body else ""
@classmethod
def mask_user_input(cls, user_input: str, command_context: Optional[str] = None) -> str:
"""
Mask sensitive information in user input based on context.
Args:
user_input: Raw user input string
command_context: Optional context about what command is being executed
Returns:
User input with sensitive parts masked
Example:
>>> SecurityMasker.mask_user_input('--login admin --password MyP@ss')
'--login admin --password ******'
"""
if not user_input:
return user_input
# Split input into tokens for analysis
tokens = user_input.split()
# Apply command argument masking
masked_tokens = cls.mask_command_arguments(tokens)
return " ".join(masked_tokens)
# Private helper methods
@classmethod
def _is_sensitive_cli_flag(cls, arg: str) -> bool:
"""Check if an argument is a sensitive command-line flag."""
return arg.lower() in {flag.lower() for flag in cls.SENSITIVE_CLI_KEYWORDS}
@classmethod
def _has_inline_sensitive_value(cls, arg: str) -> bool:
"""Check if an argument contains inline sensitive value (flag=value)."""
if "=" not in arg:
return False
flag = arg.split("=")[0]
return cls._is_sensitive_cli_flag(flag)
@classmethod
def _mask_inline_argument(cls, arg: str) -> str:
"""Mask the value part of an inline argument."""
return re.sub(r"=(.*)", f"={cls.MASK_VALUE}", arg)
@classmethod
def _mask_headers_dict(cls, headers: Dict[str, str]) -> Dict[str, str]:
"""Mask sensitive headers in dictionary format."""
masked = {}
for key, value in headers.items():
if cls._is_sensitive_header(key):
# Determine if complete or partial masking
masked[key] = cls._mask_value(value, key, is_header=True)
else:
masked[key] = value
return masked
@classmethod
def _mask_headers_list(cls, headers: List[tuple]) -> List[tuple]:
"""Mask sensitive headers in list of tuples format."""
masked = []
for item in headers:
if isinstance(item, (list, tuple)) and len(item) >= 2:
key, value = item[0], item[1]
if cls._is_sensitive_header(key):
masked_value = cls._mask_value(value, key, is_header=True)
masked.append((key, masked_value))
else:
masked.append(item)
else:
masked.append(item)
return masked
@classmethod
def _mask_headers_string(cls, headers: str) -> str:
"""Mask sensitive headers in string format."""
lines = headers.split("\n")
masked_lines = []
for line in lines:
if ":" in line:
key, value = line.split(":", 1)
if cls._is_sensitive_header(key.strip()):
# Preserve leading whitespace (indentation) from original key
leading_space = key[: len(key) - len(key.lstrip())]
masked_value = cls._mask_value(value.strip(), key.strip(), is_header=True)
masked_lines.append(f"{leading_space}{key.strip()}: {masked_value}")
else:
masked_lines.append(line)
else:
masked_lines.append(line)
return "\n".join(masked_lines)
@classmethod
def _mask_headers_object(cls, headers: Any) -> str:
"""Mask sensitive headers in object format (e.g., HTTPMessage)."""
try:
# Try to convert to dictionary first
if hasattr(headers, "items"):
header_dict = dict(headers.items())
masked_dict = cls._mask_headers_dict(header_dict)
return str(masked_dict)
elif hasattr(headers, "__dict__"):
return cls._mask_headers_dict(headers.__dict__)
else:
return "[HEADERS MASKED - UNKNOWN FORMAT]"
except Exception:
return "[HEADERS MASKED DUE TO PROCESSING ERROR]"
@classmethod
def _is_sensitive_header(cls, header_name: str) -> bool:
"""Check if a header name is considered sensitive."""
header_lower = header_name.lower().strip()
return any(sensitive in header_lower for sensitive in cls.SENSITIVE_HEADERS)
@classmethod
def _mask_string_body(cls, body: str) -> str:
"""Mask sensitive information in string body."""
try:
# Try to parse as JSON
if body.strip().startswith(("{", "[")):
json_body = json.loads(body)
masked_body = cls._mask_dict_body(json_body)
return json.dumps(masked_body)
else:
# Handle form data or other string formats
return cls._mask_form_data(body)
except json.JSONDecodeError:
# Not JSON, treat as form data or plain text
return cls._mask_form_data(body)
@classmethod
def _mask_bytes_body(cls, body: bytes) -> bytes:
"""Mask sensitive information in bytes body."""
try:
# Convert to string, mask, then back to bytes
string_body = body.decode("utf-8")
masked_string = cls._mask_string_body(string_body)
return masked_string.encode("utf-8")
except UnicodeDecodeError:
# If can't decode, return as-is (likely binary data)
return body
@classmethod
def _mask_dict_body(cls, body: Dict[str, Any]) -> Dict[str, Any]:
"""Mask sensitive information in dictionary body."""
masked = copy.deepcopy(body)
def mask_recursive(obj):
if isinstance(obj, dict):
for key, value in obj.items():
if cls._is_sensitive_body_field(key):
# Apply appropriate masking based on field type
obj[key] = cls._mask_value(str(value) if value else "", key, is_header=False)
else:
mask_recursive(value)
elif isinstance(obj, list):
for item in obj:
mask_recursive(item)
mask_recursive(masked)
return masked
@classmethod
def _mask_form_data(cls, data: str) -> str:
"""Mask sensitive information in form data."""
# Handle URL-encoded form data
if "&" in data and "=" in data:
pairs = data.split("&")
masked_pairs = []
for pair in pairs:
if "=" in pair:
key, value = pair.split("=", 1)
if cls._is_sensitive_body_field(key):
masked_pairs.append(f"{key}={cls.MASK_VALUE}")
else:
masked_pairs.append(pair)
else:
masked_pairs.append(pair)
return "&".join(masked_pairs)
# For other string formats, return as-is
return data
@classmethod
def _is_sensitive_body_field(cls, field_name: str) -> bool:
"""Check if a body field name is considered sensitive."""
field_lower = field_name.lower().strip()
return field_lower in cls.SENSITIVE_BODY_FIELDS
@classmethod
def _mask_value(cls, value: str, field_name: str, is_header: bool = False) -> str:
"""
Mask a sensitive value with either complete or partial masking.
Args:
value: The value to mask
field_name: The field/header name (for determining mask type)
is_header: Whether this is a header (vs body field)
Returns:
Masked value (either completely or partially)
"""
if not value:
return cls.MASK_VALUE
field_lower = field_name.lower().strip()
# Determine if we should use complete or partial masking
use_complete_mask = False
if is_header:
# Check if header requires complete masking
use_complete_mask = field_lower in {h.lower() for h in cls.COMPLETE_MASK_HEADERS}
# Special case: Authorization header with "Basic" is password-based
if field_lower == "authorization" and value.strip().lower().startswith("basic"):
use_complete_mask = True
else:
# Check if body field requires complete masking
use_complete_mask = field_lower in {f.lower() for f in cls.COMPLETE_MASK_FIELDS}
# Apply appropriate masking
if use_complete_mask:
return cls.MASK_VALUE
else:
# Partial masking: show last N characters
return cls._partial_mask(value)
@classmethod
def _partial_mask(cls, value: str, show_chars: int = None) -> str:
"""
Apply partial masking to a value, showing only the last N characters.
Args:
value: The value to mask
show_chars: Number of characters to show (default: PARTIAL_MASK_SHOW_CHARS)
Returns:
Partially masked value like "******a0fb"
"""
if show_chars is None:
show_chars = cls.PARTIAL_MASK_SHOW_CHARS
if not value or len(value) <= show_chars:
# If value is too short, completely mask it
return cls.MASK_VALUE
# Show last N characters
visible_part = value[-show_chars:]
return f"{cls.MASK_VALUE}{visible_part}"
# Convenience functions for backward compatibility and easy usage
def mask_passwords(args: List[str]) -> List[str]:
"""Backward compatible function for masking passwords in command arguments."""
return SecurityMasker.mask_command_arguments(args)
def mask_http_request_data(method: str, path: str, headers: Any, body: Any) -> tuple:
"""
Mask all sensitive data in an HTTP request for logging.
Args:
method: HTTP method
path: Request path/URL
headers: Request headers
body: Request body
Returns:
Tuple of (method, masked_path, masked_headers, masked_body)
"""
masked_path = SecurityMasker.mask_url_parameters(path)
masked_headers = SecurityMasker.mask_http_headers(headers)
masked_body = SecurityMasker.mask_simple_body(body)
return method, masked_path, masked_headers, masked_body
def mask_user_input(user_input: str, command_context: Optional[str] = None) -> str:
"""Convenience function for masking user input."""
return SecurityMasker.mask_user_input(user_input, command_context)