iLO-manager first main commitgit status
This commit is contained in:
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
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Utilities to simplify communicating with iLO locally
|
||||
"""
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
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."""
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
Reference in new issue
Block a user