From 9e54c219a857f58c98640e823a520e03d0a03a40 Mon Sep 17 00:00:00 2001 From: Prabhakar Pujeri Date: Thu, 26 Aug 2021 08:07:13 -0400 Subject: [PATCH] Adding redfish based system managment --- salt/modules/redfish.py | 1886 ++++++++++++++++++++ salt/proxy/redfish.py | 317 ++++ salt/states/redfish.py | 447 +++++ tests/pytests/unit/modules/test_redfish.py | 854 +++++++++ 4 files changed, 3504 insertions(+) create mode 100644 salt/modules/redfish.py create mode 100644 salt/proxy/redfish.py create mode 100644 salt/states/redfish.py create mode 100644 tests/pytests/unit/modules/test_redfish.py diff --git a/salt/modules/redfish.py b/salt/modules/redfish.py new file mode 100644 index 00000000000..8e6bdc8ab7e --- /dev/null +++ b/salt/modules/redfish.py @@ -0,0 +1,1886 @@ +# Import Python Libs +from __future__ import absolute_import, print_function, unicode_literals +import os +import json +import gzip +import re +import time +import logging +import xml.etree.ElementTree as ET +import hashlib +import pathlib +import requests +import salt.utils.platform + + +# Module to get the details of the system +log = logging.getLogger(__name__) +__virtualname__ = "redfish" +__proxyenabled__ = ["redfish"] + + +def __virtual__(): + """ + Only work on proxy + """ + if salt.utils.platform.is_proxy(): + return __virtualname__ + return (False, "The redfish module failed to load only available on proxy minions.") + + +# Module to get the boot devices present in the system +def system_get_current_boot_devices(): + """ + This module is used to get the details of the boot options + present in the current system + + Return: list of allowable boot values supported by the system + + CLI example: + .. code-block:: bash + salt '*' redfish.system_get_current_boot_devices + """ + boot_device = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/" + ) + # boot_device = __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1/") + if "Boot" in boot_device: + return boot_device["Boot"]["BootSourceOverrideTarget@Redfish.AllowableValues"] + return boot_device + + +# Module to identify reset types provided by the system +def system_get_reset_type(): + """ + This module is used to provide different reset options + present in the current system + + Return: allowable reset types supported by the system + + CLI example: + .. code-block:: bash + salt '*' redfish.system_get_reset_type + """ + reset_type = __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1/") + if "Actions" in reset_type: + return reset_type["Actions"]["#ComputerSystem.Reset"][ + "ResetType@Redfish.AllowableValues" + ] + return reset_type + + +# Module to display boot details +def system_get_boot_options(): + """ + This module is used to get the detailed information of the + supported boot options in the system + + Return: dictionary with the detailed information of boot devices + + CLI example: + .. code-block:: bash + salt '*' redfish.system_get_boot_options + """ + options = {} + boot_options = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/BootOptions" + ) + if "Members" in boot_options: + for i in boot_options["Members"]: + logging.debug(i) + boot_option_key = os.path.basename(i["@odata.id"]) + boot_option_value = __proxy__["redfish.http_get"](i["@odata.id"]) + logging.debug(boot_option_value) + if boot_option_value: + options[boot_option_key] = boot_option_value + else: + return boot_option_value + return options + + return boot_options + + +# Module to get the system power info +def system_get_psu_info(): + """ + This module is used to get the details of the power source information + of the present system. + + Return: Dictionary with the sources of power as key and + detailed information as the value + + CLI example: + .. code-block:: bash + salt '*' redfish.system_get_psu_info + """ + psu = {} + sys_info = __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1/") + if "Links" in sys_info: + for i in sys_info["Links"]["PoweredBy"]: + psu_key = os.path.basename(i["@odata.id"]) + psu_value = __proxy__["redfish.http_get"](i["@odata.id"]) + if psu_value: + psu[psu_key] = psu_value + else: + return psu_value + return psu + return sys_info + + +# Module to get thermal details of chassis +def system_get_chassis_cooling_devices(): + """ + This module is used to get the information of the processor fans used + to cool the chassis of the system + + Return: Dictionary with the cooling device as key and + its information as the value + + CLI example: + .. code-block:: bash + salt '*' redfish.system_get_chassis_cooling_devices + """ + + cooling_devices = {} + sys_info = __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1/") + if "Links" in sys_info: + for i in sys_info["Links"]["CooledBy"]: + cooling_device_key = os.path.basename(i["@odata.id"]) + cooling_device_value = __proxy__["redfish.http_get"](i["@odata.id"]) + if cooling_device_value: + cooling_devices[cooling_device_key] = cooling_device_value + else: + return cooling_device_value + return cooling_devices + return sys_info + + +# Module to get the details of CPUs of the system +def system_get_processor_info(): + """ + This module is used to get the details of the processors present + in the system + + Return: Dictionary with processor(CPU) name as key and + its information as value + + CLI example: + .. code-block:: bash + salt '*' redfish.system_get_processor_info + """ + processors = {} + proc = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/Processors" + ) + if "Members" in proc: + for i in proc["Members"]: + processor_key = os.path.basename(i["@odata.id"]) + processor_value = __proxy__["redfish.http_get"](i["@odata.id"]) + processors[processor_key] = processor_value + return processors + return proc + + +# Module to get secure boot information +def system_get_secureboot_info(): + """ + This module is used to provide the secure boot details of the system + + Return: Information of the secure booting in the system + + CLI example: + .. code-block:: bash + salt '*' redfish.system_get_secureboot_info + """ + secure_boot = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/SecureBoot" + ) + return secure_boot + + +# Module to get the storage controllers in the system +def system_get_storage_controllers_info(): + """ + This module is used to get the storage devices present in the system + + Return: Dictionary with storage controller name as key and + disks information of the controller as values. + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_storage_controllers_info + """ + ctrl_info = {} + ctrls = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/Storage" + ) + if "Members" in ctrls: + for i in ctrls["Members"]: + ctrl_key = os.path.basename(i["@odata.id"]) + ctrl_value = __proxy__["redfish.http_get"](i["@odata.id"]) + ctrl_info[ctrl_key] = ctrl_value + return ctrl_info + return ctrls + + +# Module to get the disks info in the system +def system_get_disk_drive_info(): + """ + This module is used to get the information of the each disk of + different storage controllers present in the system + + Return: Dictionary with each disk name as key and + disk information as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_disk_drive_info + """ + + disk_info = {} + ctrls = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/Storage" + ) + if ctrls: + for i in ctrls["Members"]: + ctrl_key = os.path.basename(i["@odata.id"]) + ctrl_value = __proxy__["redfish.http_get"](i["@odata.id"]) + disk_info[ctrl_key] = {} + for j in ctrl_value["Drives"]: + disk_key = os.path.basename(j["@odata.id"]) + disk_value = __proxy__["redfish.http_get"](j["@odata.id"]) + disk_info[ctrl_key][disk_key] = disk_value + return disk_info + return ctrls + + +# Module to get the virtual disks info in the system +def system_get_virtual_disk_info(): + """ + This module is used to get the information of the virtual disks of + different storage controllers if present in the system + + Return: Dictionary with each virtual disk name as key and + disk information as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_virtual_disk_info + """ + vds_info = {} + ctrls = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/Storage" + ) + if ctrls: + for i in ctrls["Members"]: + ctrl_key = os.path.basename(i["@odata.id"]) + vds_info[ctrl_key] = {} + volumes = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/Storage/{ctrl_key}/Volumes".format( + ctrl_key=ctrl_key + ) + ) + for j in volumes["Members"]: + vd_key = os.path.basename(j["@odata.id"]) + vd_value = __proxy__["redfish.http_get"](j["@odata.id"]) + vds_info[ctrl_key][vd_key] = vd_value + return vds_info + return ctrls + + +def get_lc_logs(): + """ + This module is used to get life cycle controler logs. + + Return: Dictionary with life cycle controller logs + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_lc_logs + """ + return __proxy__["redfish.http_get"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Lclog" + ) + + +def get_sel_logs(): + """ + This module is used to get System Event Log (SEL). + + Return: Dictionary with life cycle controller logs + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_sel_logs + """ + return __proxy__["redfish.http_get"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Sel" + ) + + +def get_fault_logs(): + """ + This module is used to get fault logs. + + Return: Dictionary with life cycle controller logs + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_fault_logs + """ + return __proxy__["redfish.http_get"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/FaultList" + ) + + +# Module to get details of the removable disks +def manager_get_virtual_media_removable_disk(): + """ + This module is used to provide the removable disk details + + Return: Description of the removable disks present in the system + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_get_virtual_media_removable_disk + """ + + return __proxy__["redfish.http_get"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/RemovableDisk" + ) + + +# Module to get details of cd/dvd +def manager_get_virtual_media_cd(): + """ + This module is used to provide the cd/dvd details + + Return: Description of the cd/dvd present in the system + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_get_virtual_media_cd + """ + + return __proxy__["redfish.http_get"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/CD" + ) + + +# Module to get network adapters +def system_get_network_adapters(): + """ + This module is used to extract the details of network adapters + + Return: Dictionary with each adapter name as key and + their functions as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_network_adapters + """ + adapters = {} + ret = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters" + ) + if "Members" in ret: + for i in ret["Members"]: + adapter_key = os.path.basename(i["@odata.id"]) + adapter_value = __proxy__["redfish.http_get"](i["@odata.id"]) + logging.debug(adapter_value) + adapters[adapter_key] = adapter_value + return adapters + return ret + + +# Module to get details of network adapter functions +def system_get_network_adapter_device_functions(adapter): + """ + This module is used to extract the details of the functions provided + by the specified network adapter + + Param: + - adapter: name of the adapter whose functions are required + + Return: Dictionary with each function name as key and + description of the function as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_network_adapter_device_functions + """ + + funs = {} + ret = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters/{adapter}/NetworkDeviceFunctions".format( + adapter=adapter + ) + ) + if "Members" in ret: + for i in ret["Members"]: + fun_key = os.path.basename(i["@odata.id"]) + fun_value = __proxy__["redfish.http_get"](i["@odata.id"]) + funs[fun_key] = fun_value + return funs + return ret + + +# Module to get ethernet devices in the system +def system_get_ethernet_devices(): + """ + This module is used to extract the details of the network interfaces + present in the system + + Return: Dictionary with each ethernet device name as key and + their description as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_ethernet_devices + """ + ethers = {} + ret = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces" + ) + if "Members" in ret: + for i in ret["Members"]: + ether_key = os.path.basename(i["@odata.id"]) + ether_value = __proxy__["redfish.http_get"](i["@odata.id"]) + ethers[ether_key] = ether_value + return ethers + return ret + + +# Module to reset the bios to default +def system_bios_reset_to_default(): + """ + This module is used to reset the bios to default settings in the system + + Return: details of the post command after resetting the bios + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_bios_reset_to_default + """ + reset_to_default = __proxy__["redfish.http_post"]( + uri="/redfish/v1/Systems/System.Embedded.1/Bios/Actions/Bios.ResetBios", + headers={"content-type": "application/json"}, + payload={}, + ) + if reset_to_default: + restart = system_reset(reset_type="ForceRestart") + if restart: + time.sleep(300) + return {"result": True, "message": "BIOS reset to default successfully"} + + return {"result": False, "message": "BIOS reset to default failed"} + + +# Module to reset the system +def system_reset(reset_type): + """ + This module is used to reset the system based on the reset type specified + + Param: + - reset_type: type of reset required by user and supported by system + + Return: Description of the post command after execution + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_reset reset_type=ForceOff + """ + reset_values = [ + "On", + "ForceOff", + "ForceRestart", + "GracefulShutdown", + "PushPowerButton", + "Nmi", + ] + if reset_type in reset_values: + payload = {} + headers = {} + uri = "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset" + payload["ResetType"] = reset_type + headers["content-type"] = "application/json" + ret = __proxy__["redfish.http_post"](uri, headers=headers, payload=payload) + return ret + logging.error("reset_type in not valid") + return False + + +# Module to attach image url to CD +def manager_attach_cd(image_url, force=False): + """ + This module is used to attach the image url of the required OS by the user + by means of CD + + Param: + - image_url: url location of the required OS + - force: boolean value, if it is true check if any image is already exists + and remove it and insert new image url else directly attach it + + Return: Description of the post command after execution + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_attach_cd image_url=http://100.98.4.4/suse/SLES-15-SP1.iso force=true + """ + + if force: + cd_status = manager_get_virtual_media_cd() + if "Inserted" in cd_status: + if cd_status["Inserted"]: + cd_eject = manager_eject_cd() + time.sleep(5) + if not cd_eject: + return cd_eject + uri = "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/CD/Actions/VirtualMedia.InsertMedia" + payload = { + "Image": "{image_url}".format(image_url=image_url), + "Inserted": True, + "WriteProtected": True, + } + headers = {"content-type": "application/json"} + ret = __proxy__["redfish.http_post"](uri=uri, headers=headers, payload=payload) + if ret: + ret["changes"] = "Attached {image_url} ".format(image_url=image_url) + return ret + + +# Module to attach image url to removable disk +def manager_attach_removable_disk(image_url, force=False): + """ + This module is used to attach the image url of the required OS by the user + by means of removable disk + + Param: + - image_url: url location of the required OS + + Return: Description of the post command after execution + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_attach_cd image_url=http://100.98.4.4/suse/OEMDRV.img force=true + """ + if force: + cd_status = manager_get_virtual_media_removable_disk() + if "Inserted" in cd_status: + if cd_status["Inserted"]: + cd_eject = manager_eject_removable_disk() + time.sleep(5) + if not cd_eject: + return cd_eject + + uri = "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/RemovableDisk/Actions/VirtualMedia.InsertMedia" + payload = {"Image": image_url, "Inserted": True, "WriteProtected": True} + headers = {"content-type": "application/json"} + ret = __proxy__["redfish.http_post"](uri=uri, headers=headers, payload=payload) + if ret: + ret["changes"] = "Attached {image_url} ".format(image_url=image_url) + return ret + + +# Module to eject the CD +def manager_eject_cd(): + """ + This module is used to eject the media attached through CD + + Return: Description of the post command after execution + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_eject_cd + """ + cd_status = manager_get_virtual_media_cd() + if "Inserted" in cd_status: + if not cd_status["Inserted"]: + return True + uri = "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/CD/Actions/VirtualMedia.EjectMedia" + payload = {} + headers = {"content-type": "application/json"} + ret = __proxy__["redfish.http_post"](uri=uri, headers=headers, payload=payload) + if ret: + return True + return False + return False + + +# Module to eject the removable disk +def manager_eject_removable_disk(): + """ + This module is used to eject the media attached through removable disk + + Return: Description of the post command after execution + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_eject_removable_disk + """ + + uri = "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/RemovableDisk/Actions/VirtualMedia.EjectMedia" + payload = {} + headers = {"content-type": "application/json"} + ret = __proxy__["redfish.http_post"](uri=uri, headers=headers, payload=payload) + if ret: + ret["changes"] = "Ejected Removable Disk " + return ret + + +# Module to get the firmware of the system +def get_firmware_inventory(fw_type=None): + """ + This module is used to extract the details of the firmware in the system + + Param: + - fw_type: To specify which type of firmware is requied whether + previous, current or installed. If None, all are returned. + + Return: Dictionary with the required type of firmware specified by user + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_firmware_inventory type=installed + """ + + inventory = {} + inv_type = {} + ret = __proxy__["redfish.http_get"]("/redfish/v1/UpdateService/FirmwareInventory") + if "Members" in ret: + for i in ret["Members"]: + fw_key = os.path.basename(i["@odata.id"]) + fw_value = __proxy__["redfish.http_get"](i["@odata.id"]) + inventory[fw_key] = fw_value + if fw_type is None: + return inventory + for i in inventory.keys(): + if fw_type in i: + inv_type[i] = inventory[i] + return inv_type + return ret + + +# Module to set the one time boot device +def system_onetime_boot(boot_mode, boot_device): + """ + This module is used to set the one time boot device of the system for next boot + + Param: + - name: Name of the module + - boot_mode: to define either bios or uefi mode + - boot_device: to define the device from where to boot + + Return: Description of the post command after exeution + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_Onetime_Boot name="Onetimeboot" + boot_mode="Uefi" boot_device="Pxe" + """ + + uri = "/redfish/v1/Systems/System.Embedded.1" + headers = {"content-type": "application/json"} + + if boot_mode == "Bios": + payload = {"Boot": {"BootSourceOverrideTarget": boot_device}} + elif boot_mode == "Uefi": + payload = { + "Boot": { + "BootSourceOverrideTarget": "UefiTarget", + "UefiTargetBootSourceOverride": boot_device, + } + } + ret = __proxy__["redfish.http_patch"](uri, headers, payload) + if ret: + return True + return False + + +def system_onetime_boot_device_uefi(boot_device): + """ + This module is used to set the one time boot device of the system for next boot + + Param: + - name: Name of the module + - boot_mode: to define either bios or uefi mode + - boot_device: to define the device from where to boot + + Return: Description of the post command after exeution + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_Onetime_Boot name="Onetimeboot" + boot_mode="Uefi" boot_device="Pxe" + """ + + uri = "/redfish/v1/Systems/System.Embedded.1" + headers = {"content-type": "application/json"} + payload = { + "Boot": { + "BootSourceOverrideTarget": "UefiTarget", + "UefiTargetBootSourceOverride": boot_device, + } + } + ret = __proxy__["redfish.http_patch"](uri, headers, payload) + if ret: + return True + return False + + +# Module to get the bios attributes +def system_get_bios_attributes(): + """ + This is used to get the attributes of BIOS required by the user + + :param: + - attribute_names: list of attibutes required by the user + if all attributes are required pass attribute_names="all" + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_bios_attributes attribute_names="all" + """ + return __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1/Bios") + + +# Module to get the system info +def get_system_info(): + """ + This module is used to extract the current system information + + Return: Description of the system + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_system_info + """ + return __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1") + + +# Module to get the boot order of the system +def system_get_boot_order(): + """ + This module is used to get the booting order of the system + + Return: Boot order of the system + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_boot_order + """ + boot = __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1") + if "Boot" in boot: + return boot["Boot"]["BootOrder"] + return boot + + +# Module to set the first boot device +def system_set_first_boot_device(device): + """ + This module is to set the boot order and make the device specified by the + user in first position + + Param: + - device: name of the device that is to be booted from for the first time + + Return: Description of the patch command after execution + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_set_first_boot_device device="Pxe" + """ + + first_device = None + sysinfo = __proxy__["redfish.http_get"]("/redfish/v1/Systems/System.Embedded.1/") + boot_list = sysinfo["Boot"]["BootOrder"] + boot_option = system_get_boot_options() + if boot_option: + for option in boot_option: + if device.lower() in boot_option[option]["DisplayName"].lower(): + first_device = os.path.basename(boot_option[option]["@odata.id"]) + break + boot_list = [first_device] + boot_list + payload = {"Boot": {"BootOrder": boot_list}} + headers = {"content-type": "application/json"} + return __proxy__["redfish.http_patch"]( + uri="/redfish/v1/Systems/System.Embedded.1/", headers=headers, payload=payload + ) + + +# module to provide chassis information +def get_chassis_info(): + """ + This module is provided chassis information + + + Return: dictionary of chassis information + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_chassis_info + """ + return __proxy__["redfish.http_get"]("/redfish/v1/Chassis/System.Embedded.1") + + +def chassis_reset(reset_type): + """ + This module is to reset chassis + + Param: + - device: type of chasis reset + + Return: Description of the patch command after execution + + CLI Example: + .. code-block:: bash + salt '*' redfish.chassis_reset reset_type="ForceOff" + """ + if reset_type in ["On", "ForceOff"]: + payload = {} + headers = {} + uri = "/redfish/v1/Chassis/System.Embedded.1/Actions/Chassis.Reset" + payload["ResetType"] = reset_type + headers["content-type"] = "application/json" + ret = __proxy__["redfish.http_post"](uri, headers=headers, payload=payload) + return ret + logging.error("reset_type in not valid") + return False + + +def chassis_get_assembly_info(): + """ + This module is to get chasis parts information + + Return: list of chasis part detail + + CLI Example: + .. code-block:: bash + salt '*' redfish.chassis_get_assembly_info + """ + return __proxy__["redfish.http_get"]( + "/redfish/v1/Chassis/System.Embedded.1/Assembly" + ) + + +def chassis_get_cooled_by_info(): + """ + This module is to get chasis cooled by devices + + Return: list of chassis cooling devices + + CLI Example: + .. code-block:: bash + salt '*' redfish.chassis_get_cooled_by_info + """ + chassis_info = __proxy__["redfish.http_get"]( + "/redfish/v1/Chassis/System.Embedded.1/" + ) + if "Links" in chassis_info: + return chassis_info["Links"]["CooledBy"] + logging.error("failed to get info from system") + return chassis_info + + +def chassis_get_powered_by_info(): + """ + This module is to get chasis powered devices + + Return: list of chassis power supply devices + + CLI Example: + .. code-block:: bash + salt '*' redfish.chassis_get_powered_by_info + """ + chassis_info = __proxy__["redfish.http_get"]( + "/redfish/v1/Chassis/System.Embedded.1/" + ) + if "Links" in chassis_info: + return chassis_info["Links"]["PoweredBy"] + logging.error("failed to get info from system") + return chassis_info + + +def chassis_get_power_control_info(): + """ + This module provide detailed information about power control of chassis + + Return: chassis power control information + + CLI Example: + .. code-block:: bash + salt '*' redfish.chassis_get_power_control_info + """ + power_control = __proxy__["redfish.http_get"]( + "/redfish/v1/Chassis/System.Embedded.1/Power" + ) + if "PowerControl" in power_control: + return power_control["PowerControl"] + return power_control + + +def chassis_get_power_consumed_watts(): + """ + This module provide detailed information about power consumed by system in watts + + Return: chassis power consumed in watts + + CLI Example: + .. code-block:: bash + salt '*' redfish.chassis_get_power_consumed_watts + """ + + power_control = __proxy__["redfish.http_get"]( + "/redfish/v1/Chassis/System.Embedded.1/Power" + ) + if "PowerControl" in power_control: + return power_control["PowerControl"][0]["PowerConsumedWatts"] + return power_control + + +def update_service_get_fw_components(comp_type=None): + """ + This module provide firmware components + Param: + - comp_type: type of firmware component EX for BIOS component id is 159 + + Return: chassis power consumed in watts + + CLI Example: + .. code-block:: bash + salt '*' redfish.chassis_get_power_consumed_watts comp_type=159 + """ + components = [] + firmware_inv = __proxy__["redfish.http_get"]( + "/redfish/v1/UpdateService/FirmwareInventory" + ) + if firmware_inv["Members"]: + for i in firmware_inv["Members"]: + if comp_type: + if comp_type in os.path.basename(i["@odata.id"]): + components.append(os.path.basename(i["@odata.id"]).split("-")[1]) + else: + components.append(os.path.basename(i["@odata.id"]).split("-")[1]) + return components + return firmware_inv + + +# Module to download and parse the catalogue file +def parse_catalogue_file(system, components): + """ + This is used to get the details of the specified dell poweredge server components. + We can get the catalogue file from downloads.dell.com where whole dell components + information is available. + + Params: + - system : specify which poweredge server + - components : a list of components whose information is required + + Return: Dictionary with component_id as key and + result as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.parse_catalogue_file system="R730" + components=[159,25227] + """ + url = "http://downloads.dell.com/catalog/Catalog.gz" + filename = url.split("/")[-1] + with open(filename, "wb") as file_d: + req = requests.get(url) + file_d.write(req.content) + file_d.close() + + data = gzip.open(filename, "r") + tree = ET.parse(data) + + comp = {} + for soft_comp in tree.findall("SoftwareComponent"): + for supp_device in soft_comp.findall("SupportedDevices"): + for device in supp_device.findall("Device"): + for i in range(len(components)): + if supp_device.get("componentID") == str(components[i]): + for supp_sys in soft_comp.findall("SupportedSystems"): + for brand in supp_sys.findall("Brand"): + for model in brand.findall("Model"): + for display in model.findall("Display"): + if ( + display.text == system + and os.path.basename( + soft_comp.get("path") + ).split(".")[-1] + == "EXE" + ): + comp[str(components[i])] = { + "path": soft_comp.get("path"), + "version": soft_comp.get( + "vendorVersion" + ), + "result": True, + "rebootRequired": soft_comp.get( + "rebootRequired" + ), + "hashMD5": soft_comp.get("hashMD5"), + } + + for i in range(len(components)): + if str(components[i]) not in comp.keys(): + comp[str(components[i])] = { + "path": "Not found", + "version": "Not found", + "result": False, + "rebootRequired": "None", + "hashMD5": "None", + } + + return comp + + +# Module to know the current version of the specified components +def get_current_version(component_ids): + """ + This is used to return the details of the current version of the specified components required by the user. + If the specified componet is found results will be displayed else it will be displayed as not found. + + Params: + - component_ids: It is a list to provide the IDs of required components + + Return: Dictionary with component_id as key and + result as value + + CLI Example: + .. code-block:: bash + salt-call '*' redfish.get_current_version component_ids=[159,101277,252752] + """ + fws = {} + inventory = __proxy__["redfish.http_get"]( + "/redfish/v1/UpdateService/FirmwareInventory" + ) + if inventory: + for comp_id in component_ids: + for i in inventory["Members"]: + inv_detail = __proxy__["redfish.http_get"](i["@odata.id"]) + # logging.debug(inv_detail) + if inv_detail: + if str(comp_id) == str(inv_detail["SoftwareId"]): + fws[comp_id] = { + "name": inv_detail["Name"], + "version": inv_detail["Version"], + } + else: + continue + return inv_detail + return fws + return inventory + + +# Module to know the previous version of the specified components +def get_previous_version(component_ids): + """ + This is used to return the details of the previous version of the specified components required by the user. + If the specified componet is found results will be displayed else it will be displayed as not found. + + Params: + - component_ids: It is a list to provide the IDs of required components + + Return: Dictionary with component_id as key and + result as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_previous_version component_ids=[159,101277,252752] + """ + fws = {} + inv = __proxy__["redfish.http_get"]("/redfish/v1/UpdateService/FirmwareInventory") + if inv: + for comp_id in component_ids: + for i in inv["Members"]: + if "Previous" in os.path.basename(i["@odata.id"]): + inv_detail = __proxy__["redfish.http_get"](i["@odata.id"]) + if inv_detail: + if str(comp_id) == str(inv_detail["SoftwareId"]): + fws[comp_id] = { + "name": inv_detail["Name"], + "version": inv_detail["Version"], + } + else: + continue + else: + return inv_detail + else: + continue + return fws + return inv + + +def get_avaiable_fw_versions(component_ids): + """ + This is used to return the details of the avaiable version of the specified components required by the user. + If the specified componet is found results will be displayed else it will be displayed as not found. + + Params: + - component_ids: It is a list to provide the IDs of required components + + Return: Dictionary with component_id as key and + result as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_avaiable_fw_versions component_ids=[159,101277,252752] + """ + + fws = {} + inv = __proxy__["redfish.http_get"]("/redfish/v1/UpdateService/FirmwareInventory") + if inv: + for comp_id in component_ids: + for i in inv["Members"]: + if "Available" in os.path.basename(i["@odata.id"]): + inv_detail = __proxy__["redfish.http_get"](i["@odata.id"]) + if inv_detail: + if str(comp_id) == str(inv_detail["SoftwareId"]): + fws[comp_id] = { + "name": inv_detail["Name"], + "version": inv_detail["Version"], + "uri": inv_detail["@odata.id"], + } + else: + continue + else: + return inv_detail + else: + continue + return fws + return inv + + +# Module to know the installed version of the specified component +def get_installed_version(component_ids): + """ + This is used to return the details of the installed version of the specified components required by the user. + If the specified componet is found results will be displayed else it will be displayed as not found. + + Params: + - component_ids: It is a list to provide the IDs of required components + + Return: Dictionary with component_id as key and + result as value + + CLI Example: + .. code-block:: bash + salt '*' redfish.get_installed_version component_ids=[159,101277,252752] + """ + fws = {} + inv = __proxy__["redfish.http_get"]("/redfish/v1/UpdateService/FirmwareInventory") + if inv: + for comp_id in component_ids: + for i in inv["Members"]: + if "Installed" in os.path.basename(i["@odata.id"]): + inv_detail = __proxy__["redfish.http_get"](i["@odata.id"]) + if inv_detail: + if str(comp_id) == str(inv_detail["SoftwareId"]): + fws[comp_id] = { + "name": inv_detail["Name"], + "version": inv_detail["Version"], + } + else: + continue + else: + return inv_detail + else: + continue + return fws + return inv + + +def delete_fw_payload(uri): + """ + This is used to delete firmware payload + + Params: + - uri: firmware payload uri + + Return: Dictionary with status + + CLI Example: + .. code-block:: bash + salt '*' redfish.delete_fw_payload uri=/redfish/v1/UpdateService/FirmwareInventory/Previous-159-2.3.10 + """ + etag = _get_fw_etag(uri) + headers = {"If-Match": "{ETag}".format(ETag=etag)} + return __proxy__["redfish.http_delete"](uri, headers=headers) + + +def _get_fw_etag(uri): + logging.debug({"uri": uri}) + details = __proxy__["redfish.get_details"]() + session = details["session"] + host = details["host"] + inventory = session.get("https://" + host + uri) + logging.debug({"ETag": inventory.headers["ETag"]}) + return inventory.headers["ETag"] + + +# Module to download and copy the content of the file to specified path +def download_firmware(uri, md5hash): + """ + Whenever we find a new software version of a particular component, to update that + first we download it to a specific path and then install it. + + Param: + -url: Here, the url from where the software is to be downloaded will be present + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.download_firmware url="Path where the file to be downloaded is present" + """ + response = requests.get("http://downloads.dell.com/" + uri, verify=False) + try: + file_path = "/opt/" + os.path.basename(uri) + with salt.utils.files.fopen(file_path, "wb") as fw_payload_file: + fw_payload_file.write(response.content) + md5sum = hashlib.md5(pathlib.Path(file_path).read_bytes()).hexdigest() + if md5sum == md5hash: + return {"result": True, "path": file_path} + except requests.exceptions.RequestException: + return {"result": False,"comment": "Not able to download Firmware"} + + +def upload_firmware(path): + """ + This module provide functionality to upload firmware to system + + Param: + -path: local path to firmware + + Return: status of firmware upload + + CLI Example: + .. code-block:: bash + salt '*' redfish.upload_firmware path=/tmp/FW/BIOS-1.0.0.EXE + """ + etag = _get_fw_etag("/redfish/v1/UpdateService/FirmwareInventory/") + files = { + "file": ( + os.path.basename(path), + salt.utils.files.fopen(path, "rb"), + "multipart/form-data", + ) + } + headers = {"if-match": "{ETag}".format(ETag=etag)} + return __proxy__["redfish.http_post"]( + "/redfish/v1/UpdateService/FirmwareInventory", files=files, headers=headers + ) + + +# Module to update the firmware +def update_firmware(uri): + """ + This module is used to update the firmware of the component specified by the user if updated version is found in the repository. + Here, the updated version of firmware is uploaded in to the payload and will be installed on to the system. + + Params: + - uri: path where the latest version of the component is present + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.update_firmware path="Path where the file to be installed is present" + """ + payload = {"ImageURI": uri} + headers = {"content-type": "application/json"} + update = __proxy__["redfish.http_post"]( + "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate", + headers=headers, + payload=payload, + ) + return update + + +# Module to check status of the specified job +def check_job_status(job_id): + """ + This is used to check the status of the job specified by the user. + + Param: + - job_id: JID of the job whose status is to be checked + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.check_job_status job_id="JID of the job" + """ + config = __opts__["redfish"] + + while True: + uri = "/redfish/v1/TaskService/Tasks/" + req = requests.get( + config["redfish_ip"] + uri + job_id, + auth=(config["username"], config["password"]), + verify=False, + ) + data = req.json() + status_code = req.status_code + + if status_code != 202 or status_code != 200: + return {"result": False, "message": "Query job ID command failed"} + return {"result": True, "message": data["TaskState"]} + + return {"result": False, "message": "Nothing executed"} + + +# Module to reboot server +def system_reboot_server(): + """ + This module is used to reboot the server based on the user requirement. + For some of the updates in the system to be installed, it requires reboot. + At that time user can execute this module. + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_reboot_server + """ + uri = "/redfish/v1/Systems/System.Embedded.1/" + config = __opts__["redfish"] + response = requests.get( + config["redfish_ip"] + uri, + auth=(config["username"], config["password"]), + verify=False, + ) + data = response.json() + + if data["PowerState"] == "On": + url = ( + config["redfish_ip"] + + "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset" + ) + payload = {"ResetType": "GracefulShutdown"} + headers = {"content-type": "application/json"} + response = requests.post( + url, + data=json.dumps(payload), + headers=headers, + verify=False, + auth=(config["username"], config["password"]), + ) + status_code = response.status_code + if status_code != 204: + return { + "result": False, + "message": "Command failed to gracefully power OFF server", + } + time.sleep(10) + count = 0 + while True: + response = requests.get( + config["redfish_ip"] + "/redfish/v1/Systems/System.Embedded.1/", + verify=False, + auth=(config["username"], config["password"]), + ) + data = response.json() + if data["PowerState"] == "Off": + break + elif count == 20: + url = ( + config["redfish_ip"] + + "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset" + ) + payload = {"ResetType": "ForceOff"} + headers = {"content-type": "application/json"} + response = requests.post( + url, + data=json.dumps(payload), + headers=headers, + verify=False, + auth=(config["username"], config["password"]), + ) + status_code = response.status_code + if status_code != 204: + return { + "result": False, + "message": "FAIL, Command failed to gracefully power OFF server", + } + time.sleep(15) + break + else: + time.sleep(2) + count += 1 + continue + + payload = {"ResetType": "On"} + headers = {"content-type": "application/json"} + response = requests.post( + url, + data=json.dumps(payload), + headers=headers, + verify=False, + auth=(config["username"], config["password"]), + ) + status_code = response.status_code + if status_code != 204: + return { + "result": False, + "message": "- FAIL, Command failed to power ON server", + } + + elif data["PowerState"] == "Off": + url = ( + config["redfish_ip"] + + "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset" + ) + payload = {"ResetType": "On"} + headers = {"content-type": "application/json"} + response = requests.post( + url, + data=json.dumps(payload), + headers=headers, + verify=False, + auth=(config["username"], config["password"]), + ) + status_code = response.status_code + if status_code != 204: + return { + "result": False, + "message": "- FAIL, Command failed to power ON server", + } + + return {"result": True, "message": "passed"} + + else: + return { + "result": False, + "message": "- FAIL, unable to get current server power state to perform either reboot or power on", + } + + +def system_get_bios_attribute_type(attribute_name): + """ + This is used to get BIOS attribute types + + Params: + - attribute_names: BIOS attributes name + + Return: Displays BIOS attribute type + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_bios_attribute_type attribute_names="BootMode" + """ + + bios_registry = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/Bios/BiosRegistry" + ) + if bios_registry: + if "RegistryEntries" in bios_registry: + if "Attributes" in bios_registry["RegistryEntries"]: + for attrib in bios_registry["RegistryEntries"]["Attributes"]: + if attrib["AttributeName"] == attribute_name: + return attrib["Type"] + return bios_registry + + +def system_get_bios_attribute_current_value(attribute_name): + """ + This is used to get BIOS attribute current value + + Params: + - attribute_names: BIOS attribute name + + Return: Displays BIOS attribute current value + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_bios_attribute_current_value attribute_names="BootMode" + """ + + bios_attributes = __proxy__["redfish.http_get"]( + "/redfish/v1/Systems/System.Embedded.1/Bios/" + ) + if bios_attributes: + if "Attributes" in bios_attributes: + if attribute_name in bios_attributes["Attributes"]: + return bios_attributes["Attributes"][attribute_name] + return False + + +# Module to set the bios attributes +def system_set_bios_attribute(attribute_names=[], attribute_values=[]): + """ + This is used to change the bios attributes based on the user requirement by specifying the + attributes to be changed along with the values respectively. + + Params: + - attribute_names: list of the attributes whose values are to be changed + - attribute_values: list of values for corresponding attributes + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_set_bios_attributes attribute_names=["BootMode"] + attribute_values=["Uefi"] + """ + + payload = {"Attributes": {}} + for attri_name, attri_val in zip(attribute_names, attribute_values): + attrib_type = system_get_bios_attribute_type(attri_name) + if attrib_type == "Integer": + payload["Attributes"][attri_name] = int(attri_val) + else: + payload["Attributes"][attri_name] = attri_val + logging.debug(payload) + headers = {"content-type": "application/json"} + bios_uri = "/redfish/v1/Systems/System.Embedded.1/Bios/Settings" + bios_set = __proxy__["redfish.http_patch"](bios_uri, headers, payload) + if bios_set: + job_uri = "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs" + payload = { + "TargetSettingsURI": "/redfish/v1/Systems/System.Embedded.1/Bios/Settings" + } + headers = {"content-type": "application/json"} + config_job = __proxy__["redfish.http_post"](job_uri, headers, payload) + if config_job: + logging.debug(config_job["headers"]["Location"]) + job_id = config_job["headers"]["Location"] + restart = system_reset(reset_type="ForceRestart") + if restart: + for i in range(20): + job_status = bios_set = __proxy__["redfish.http_get"](job_id) + if job_status["JobState"] == "Completed": + return { "result": True, "message": "BIOS Attribute set successfully", } + time.sleep(30) + + return { + "result": False, + "message": "Attribute set failed", + "StatusCode": False, + } + + +# Module to create virtual disk +def system_create_virtual_disk(controller, disks, volume_type): + """ + This module is used to create the RAID levels with the help of the physical drives present. + + Params: + - controller: storage controller on which virtual disk is to be created + - disks: It is a string of disks separated by comma(,) to create RAID levels + - volume_type: to define the type of RAID level + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_create_virtual_disk controller="contoller that supports RAID creation" + disks="Disks for creation" volume_type:"Type of RAID to be created" + """ + config = __opts__["redfish"] + url = "/redfish/v1/Systems/System.Embedded.1/Storage/" + controller + disks_list = disks.split(",") + final_disks_list = [] + for i in disks_list: + drives_uri = "/redfish/v1/Systems/System.Embedded.1/Storage/Drives/" + i + drive_id = {"@odata.id": drives_uri} + final_disks_list.append(drive_id) + payload = {"VolumeType": volume_type, "Drives": final_disks_list} + + headers = {"Content-type": "application/json"} + response = requests.post( + config["redfish_ip"] + url + "/Volumes", + data=json.dumps(payload), + headers=headers, + verify=False, + auth=(config["username"], config["password"]), + ) + if response.status_code == 202: + pass + else: + return {"result": False, "message": "Post command failed to execute"} + + resp_header = response.headers["Location"] + try: + job_id = re.search("JID.+", resp_header).group() + except KeyError: + return {"result": False, "message": "Job_ID creation failed"} + + req = requests.get( + config["redfish_ip"] + "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/" + job_id, + auth=(config["username"], config["password"]), + verify=False, + ) + data = req.json() + if data["JobType"] == "RAIDConfiguration": + job_type = "staged" + elif data["JobType"] == "RealTimeNoRebootConfiguration": + job_type = "realtime" + return {"result": True, "Job_ID": job_id, "JobType": job_type} + + +# Module to delete virtual disk +def system_delete_virtual_disk(fqdd): + """ + This module is used to delete the virtual disks created for RAID. + + Params: + - fqdd: provide the details of the virtual disk to be deleted + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.ssytem_delete_virtual_disk fqdd="virtual disk details" + """ + config = __opts__["redfish"] + url = "/redfish/v1/Systems/System.Embedded.1/Storage/Volumes/" + response = requests.delete( + config["redfish_ip"] + url + fqdd, + verify=False, + auth=(config["username"], config["password"]), + ) + + if response.status_code == 202: + pass + else: + return { + "result": False, + "message": "Delete command failed", + "status_code": response.json(), + } + + resp_header = response.headers["Location"] + try: + job_id = re.search("JID.+", resp_header).group() + except KeyError: + return {"result": False, "message": "Job_ID failed to create"} + + req = requests.get( + config["redfish_ip"] + "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/" + job_id, + auth=(config["username"], config["password"]), + verify=False, + ) + data = req.json() + if data["JobType"] == "RAIDConfiguration": + job_type = "staged" + elif data["JobType"] == "RealTimeNoRebootConfiguration": + job_type = "realtime" + return {"result": True, "Job_ID": job_id, "Job_type": job_type} + + +def system_get_pdisks_hot_spare_type(fqdd): + """ + This module is used to physical disks of hot spare type. + + Params: + - fqdd: provide the details of the virtual disk + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_get_pdisks_hot_spare_type fqdd="virtual disk details" + """ + config = __opts__["redfish"] + url = "/redfish/v1/Systems/System.Embedded.1/Storage/" + response = requests.get( + config["redfish_ip"] + url + "/" + fqdd, + verify=False, + auth=(config["username"], config["password"]), + ) + if response.status_code != 200: + return { + "result": False, + "message": "Either controller fqdd does not exist or typo in fqdd string name", + } + data = response.json() + drive_list = [] + if data["Drives"] != []: + for i in data["Drives"]: + for drive_name in i.items(): + disk = drive_name[1].split("/")[-1] + drive_list.append(disk) + else: + return {"result": False, "message": "No drives found for specified controller"} + + drive_types = {} + for i in drive_list: + response = requests.get( + config["redfish_ip"] + + "/redfish/v1/Systems/System.Embedded.1/Storage/Drives/" + + i, + verify=False, + auth=(config["username"], config["password"]), + ) + data = response.json() + for hotspare_type in data.items(): + if hotspare_type[0] == "HotspareType": + drive_types[i] = hotspare_type[1] + return {"result": True, "Spare_type": drive_types} + + +# Module to assign hot spare +def system_assign_spare(types, hot_spare, virtual_disk): + """ + This module is used to assign hot spare to VD + + Params: + - type: provide type of assign like global or dedicated + - hot_spare: fqdd of physical disk + - virtual_disk: fqdd of virtual disk + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_assign_spare types=dedicated hot_spare=Slot-1.0.1 virtual_disk= + """ + + config = __opts__["redfish"] + url = "/redfish/v1/Dell/Systems/System.Embedded.1/DellRaidService/Actions/DellRaidService.AssignSpare" + headers = {"content-type": "application/json"} + + if types.lower() == "global": + payload = {"Targetfqdd": hot_spare} + elif types.lower() == "dedicated": + payload = {"Targetfqdd": hot_spare, "VirtualDiskArray": [virtual_disk]} + + response = requests.post( + config["redfish_ip"] + url, + data=json.dumps(payload), + headers=headers, + verify=False, + auth=(config["username"], config["password"]), + ) + if response.status_code == 202: + try: + job_id = response.headers["Location"].split("/")[-1] + except KeyError: + return {"result": False, "message": "Unable to find Job_ID"} + return {"result": True, "Job_ID": job_id} + + return {"result": False, "message": "Unable to execute post command"} + + +# Module to get the job ids in iDRAC job queue +def manager_get_jobs(): + """ + This is used to get all the job ids present in the job queue of the iDRAC + + Return: Job IDs present in the system + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_get_job_queue_ids + """ + jobs = __proxy__["redfish.http_get"]("/redfish/v1/Managers/iDRAC.Embedded.1/Jobs") + if jobs: + return jobs + return False + + +# Module to get the details of job +def manager_get_job_detail(job_id): + """ + This is used to get the details of specified job present in the job queue of the iDRAC + + Param: + - job_id: JID of the job whose details are required + + Return: Dictionary displaying the details of the specified job + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_get_job_id_details job_id="JID of job" + """ + job_detail = __proxy__["redfish.http_get"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/" + job_id + ) + if job_detail: + return job_detail + return job_detail + + +# Module to delete the job +def manager_delete_job_id(job_id): + """ + This is used to delete the specified job, if present in the job queue of iDRAC + + Param: + - job_id: JID of the job which is to be deleted + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_delete_job_id job_id="JID of job" + """ + headers = {"content-type": "application/json"} + job_del = __proxy__["redfish.http_delete"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/" + job_id, headers=headers + ) + return job_del + + +# Module to clear all the jobs +def manager_clear_jobs(): + """ + This is used to delete all the jobs present in the job queue of iDRAC + + Return: Displays whether the action is sucessful or not + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_clear_job_queue + """ + jobs = __proxy__["redfish.http_get"]("/redfish/v1/Managers/iDRAC.Embedded.1/Jobs") + if jobs: + for job_id in jobs["Members"]: + headers = {"content-type": "application/json"} + job_d = __proxy__["redfish.http_delete"]( + job_id["@odata.id"], headers=headers + ) + if job_d: + continue + return job_d + return {"result": True, "message": "Job queue is cleared successfully"} + + +def manager_get_sessions(): + """ + This function is used to get number of sessions with system manager + + + Return: Display number of session + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_get_sessions + """ + + session = __proxy__["redfish.http_get"]("/redfish/v1/SessionService/Sessions/") + if "Members" in session: + return session["Members"] + return session + + +def manager_get_host_interfaces(): + """ + This function provide list host interfaces + + + Return: Display number of host interfaces + + CLI Example: + .. code-block:: bash + salt '*' redfish.manager_get_host_interfaces + """ + + host_interfaces = __proxy__["redfish.http_get"]( + "/redfish/v1/Managers/iDRAC.Embedded.1/HostInterfaces" + ) + if host_interfaces: + return host_interfaces["Members"] + return host_interfaces diff --git a/salt/proxy/redfish.py b/salt/proxy/redfish.py new file mode 100644 index 00000000000..7ce9c054283 --- /dev/null +++ b/salt/proxy/redfish.py @@ -0,0 +1,317 @@ +from __future__ import absolute_import, print_function, unicode_literals + +# Import python libs +import logging +import requests +import salt.utils.http as http +import salt.utils.json + +# This must be present or the Salt loader won't load this module +__proxyenabled__ = ["redfish"] + +__virtualname__ = "redfish" + +# Variables are scoped to this module so we can have persistent data +# across calls to fns in here. + +GRAINS_CACHE = {} +DETAILS = {} +thisproxy = {} + +# Want logging! +log = logging.getLogger(__file__) + + +def __virtual__(): + return __virtualname__ + + +def init(opts): + + """ + This function gets called when the proxy starts up. + We check opts to see if a fallback user and password are supplied. + If they are present, and the primary credentials don't work, then + we try the backup before failing. + Whichever set of credentials works is placed in the persistent + DETAILS dictionary and will be used for further communication with the + chassis. + """ + DETAILS["host"] = __opts__["proxy"]["host"] + DETAILS["admin_username"] = __opts__["proxy"]["username"] + DETAILS["admin_password"] = __opts__["proxy"]["password"] + verify = __opts__["proxy"]["verify"] + if "cert" in __opts__["proxy"].keys(): + cert = __opts__["proxy"]["cert"] + session = requests.Session() + session.auth = (DETAILS["admin_username"], DETAILS["admin_password"]) + session.verify = False + + if verify == "true": + logging.info("SSL verification enabled") + session.verify = True + if cert is not None: + logging.info("SSL Cert: " + cert) + if "," in cert: + session.cert = [path.strip() for path in cert.split(",")] + else: + session.cert = cert + elif verify == "false": + session.verify = False + DETAILS["session"] = session + # system_services=__salt__['redfish.get_services'](DETAILS['host'],session) + # if system_services['result']: + # DETAILS['services']=system_services['result'] + log.debug(DETAILS) + return True + + +def get_details(): + return DETAILS + + +def admin_username(): + """ + Return the admin_username in the DETAILS dictionary, or root if there + is none present + """ + return DETAILS.get("admin_username", "root") + + +def admin_password(): + """ + Return the admin_password in the DETAILS dictionary, or 'calvin' + (the Dell default) if there is none present + """ + if "admin_password" not in DETAILS: + log.info("proxy.redfish: No admin_password in DETAILS, returning Dell default") + return "calvin" + + return DETAILS.get("admin_password", "calvin") + + +def host(): + return DETAILS["host"] + + +def _grains(host, user, password): + """ + Get the grains from the proxied device + """ + r = __salt__["redfish.get_SystemInfo"]( + host=host, admin_username=user, admin_password=password + ) + if r["result"]: + GRAINS_CACHE = r["result"] + else: + GRAINS_CACHE = {} + return GRAINS_CACHE + + +def grains(): + """ + Get the grains from the proxied device + """ + if not GRAINS_CACHE: + return _grains( + DETAILS["host"], DETAILS["admin_username"], DETAILS["admin_password"] + ) + + return GRAINS_CACHE + + +def grains_refresh(): + """ + Refresh the grains from the proxied device + """ + GRAINS_CACHE = {} + return grains() + + +def find_credentials(): + """ + Cycle through all the possible credentials and return the first one that + works + """ + + log.debug( + "proxy redfish.find_credentials found no valid credentials, using Dell default" + ) + return ("root", "calvin") + + +def chconfig(cmd, *args, **kwargs): + """ + This function is called by the :mod:`salt.modules.chassis.cmd ` + shim. It then calls whatever is passed in ``cmd`` + inside the :mod:`salt.modules.dracr ` + module. + :param cmd: The command to call inside salt.modules.dracr + :param args: Arguments that need to be passed to that command + :param kwargs: Keyword arguments that need to be passed to that command + :return: Passthrough the return from the dracr module. + """ + # Strip the __pub_ keys...is there a better way to do this? + for k in list(kwargs): + if k.startswith("__pub_"): + kwargs.pop(k) + + # Catch password reset + + if "redfish." + cmd not in __salt__: + ret = {"retcode": -1, "message": "redfish." + cmd + " is not available"} + else: + ret = __salt__["redfish." + cmd](DETAILS["session"], **kwargs) + + return ret + + +def ping(): + """ + Is the chassis responding? + :return: Returns False if the chassis didn't respond, True otherwise. + """ + r = __salt__["redfish.get_system_info"]() + if r.get("retcode", 0) == 1: + return False + else: + return True + try: + return r["dict"].get("ret", False) + except Exception: # pylint: disable=broad-except + return False + + +def shutdown(opts): + """ + Shutdown the connection to the proxied device. + For this proxy shutdown is a no-op. + """ + log.debug("redfish proxy shutdown() called...") + + +def http_get(uri): + """ + Internal function for HTTP GET method + + + Return: dictionary with get result + + """ + + # Make request + details = get_details() + ret = http.query( + "https://" + details["host"] + uri, + verify_ssl=False, + username=details["admin_username"], + password=details["admin_password"], + backend="requests", + ) + if ret.get("error"): + return ret + else: + if "body" in ret and ret["body"] != "": + return salt.utils.json.loads(ret.get("body")) + else: + return ret + + +def http_post(uri, headers, payload=None, files=None): + """ + Internal function for HTTP POST method + + Return: dictionary with get result + + """ + result = {} + details = get_details() + if files: + ret = http.query( + "https://" + details["host"] + uri, + method="POST", + header_dict=headers, + data_file=files, + verify_ssl=False, + username=details["admin_username"], + password=details["admin_password"], + backend="requests", + ) + else: + ret = http.query( + "https://" + details["host"] + uri, + method="POST", + header_dict=headers, + data=salt.utils.json.dumps(payload), + verify_ssl=False, + headers=True, + username=details["admin_username"], + password=details["admin_password"], + backend="requests", + ) + + if ret.get("error"): + return ret + else: + if "headers" in ret: + logging.debug(ret.get("headers")) + result["headers"] = ret.get("headers") + + if "body" in ret and ret["body"] != "": + logging.debug(ret.get("body")) + result["body"] = salt.utils.json.loads(ret.get("body")) + return result + + +def http_patch(uri, headers, payload): + """Internal function for HTTP PATCH method + + Return: dictionary with get result + + """ + + details = get_details() + ret = http.query( + "https://" + details["host"] + uri, + method="PATCH", + header_dict=headers, + data=salt.utils.json.dumps(payload), + verify_ssl=False, + username=details["admin_username"], + password=details["admin_password"], + backend="requests", + ) + if ret.get("error"): + return ret + else: + if "body" in ret: + return salt.utils.json.loads(ret.get("body")) + else: + return ret + + +def http_delete(uri, headers): + """ + internal function for HTTP PATCH method + + Return: dictionary with get result + + """ + + details = get_details() + ret = http.query( + "https://" + details["host"] + uri, + method="DELETE", + header_dict=headers, + verify_ssl=False, + username=details["admin_username"], + password=details["admin_password"], + backend="requests", + ) + if ret.get("error"): + return ret + else: + if "body" in ret: + return salt.utils.json.loads(ret.get("body")) + else: + return ret diff --git a/salt/states/redfish.py b/salt/states/redfish.py new file mode 100644 index 00000000000..01b07021cf8 --- /dev/null +++ b/salt/states/redfish.py @@ -0,0 +1,447 @@ +# -*- coding: utf-8 -*- +""" +DMTF Redfish API for managing server +The redfish module is used to create and manage servers through out of band interface + +""" +# Import Python Libs +from __future__ import absolute_import, print_function, unicode_literals +import logging + + +# Import Salt Libs +import salt.utils.json +import salt.utils.versions + + +log = logging.getLogger(__name__) + + +def __virtual__(): + """ + Only load if the bower module is available in __salt__ + """ + return "redfish" + + +def default_bios_settings(name): + """ + Set factory default settings of BIOS + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.default_bios_settings + + + """ + ret = {"name": name, "result": False, "comment": "bios reset faild", "changes": {}} + if __opts__["test"]: + ret["comment"] = "Reset BIOS setting to Default" + ret["result"] = None + return ret + + bios_reset = __salt__["redfish.system_bios_reset_to_default"]() + if bios_reset: + ret["result"] = True + ret["comment"] = "system bios reset to factory default" + ret["changes"]["ForceRestart"] = "In Progress" + ret["changes"]["BIOS"] = "Factory Default" + return ret + return ret + + +def system_power_state(name, power_state): + """ + set system power state + Params: + - power_state : set power state + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_power_state + + + """ + + ret = {"name": name, "result": False, "comment": "bios reset faild", "changes": {}} + if __opts__["test"]: + ret["comment"] = "Set Power state to {0}".format(power_state) + ret["result"] = None + return ret + + if power_state not in ["On", "Off"]: + ret["comment"] = "power state type not avaiable. supported options On|Off" + else: + sys_info = __salt__["redfish.get_system_info"]() + if sys_info: + logging.debug(sys_info["PowerState"]) + if power_state == sys_info["PowerState"]: + ret["result"] = True + ret["comment"] = "Already system in power {power_state} state".format( + power_state=power_state + ) + elif power_state == "On": + if __salt__["redfish.system_reset"](reset_type="On"): + ret["comment"] = "Powering on system" + ret["changes"]["PowerState"] = "On" + ret["result"] = True + elif power_state == "Off": + if __salt__["redfish.system_reset"](reset_type="ForceOff"): + ret["comment"] = "Powering off system" + ret["changes"]["PowerState"] = "Off" + ret["result"] = True + return ret + + +def bios_mode_to_uefi(name): + """ + This state will set BIOS boot mode to UEFI + + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.bios_mode_to_uefi" + """ + + ret = { + "name": name, + "result": False, + "comment": "faild to set UEFI mode", + "changes": {}, + } + if __opts__["test"]: + ret["comment"] = "Set to UEFI mode " + ret["result"] = None + return ret + + bios_info = __salt__["redfish.system_get_bios_attributes"]() + if "Attributes" in bios_info: + if "BootMode" in bios_info["Attributes"]: + logging.debug(bios_info) + logging.debug(bios_info["Attributes"]["BootMode"]) + if bios_info["Attributes"]["BootMode"] == "Uefi": + ret["comment"] = "Already in UEFI Mode" + ret["result"] = True + return ret + else: + bios_mode = __salt__["redfish.system_set_bios_attribute"]( + attribute_names=["BootMode"], attribute_values=["Uefi"] + ) + if bios_mode: + if "Job_id" in bios_mode: + status = __salt__["redfish.check_job_status"]() + logging.debug(status) + + +def nic_pxe_configure(name, nic_fqdd): + """ + This state will be used to set system one time boot to device + Params: + - boot_mode : system boot mode used for one time boot + - boot_device : device name for one time boot + + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_onetime_boot boot_mode="Bios" boot_device="Pxe" + """ + ret = { + "name": name, + "result": False, + "comment": "NIC FQDD not exist", + "changes": {}, + } + if __opts__["test"]: + ret["comment"] = "NIC for PXE boot {0}".format(boot_device) + ret["result"] = None + return ret + nic_adaptors = __salt__["redfish.system_get_ethernet_devices"]() + if nic_fqdd in nic_adaptors: + logging.debug("Requested Network Controller Exist: " + nic_fqdd) + if nic_adaptors[nic_fqdd]["LinkStatus"] != "LinkUp": + + ret["comment"] = "Network Port cable not connected" + ret["result"] = False + return ret + logging.debug("Requested Network Controller Cable Connected: " + nic_fqdd) + nic_name = nic_fqdd.split("-")[0] + logging.error("prabhakar" + nic_name) + nic_func = __salt__["redfish.system_get_network_adapter_device_functions"]( + nic_name + ) + if "Oem" in nic_func[nic_fqdd]: + logging.debug(nic_func[nic_fqdd]) + if ( + nic_func[nic_fqdd]["Oem"]["Dell"]["DellNICCapabilities"][ + "PXEBootSupport" + ] + == "Supported" + ): + logging.debug("Requested Network Controller Support PXE: " + nic_fqdd) + attrib = '["PxeDev1EnDis","PxeDev1Interface", "PxeDev1Protocol","PxeDev1VlanEnDis"]' + attrib_value = '["Enabled",' + nic_fqdd + ',"IPv4","Disabled"]' + logging.debug("attributes:") + logging.debug(attrib) + + logging.debug("attribute values:") + logging.debug(attrib_value) + + pxe_config = __salt__["redfish.system_set_bios_attribute"]( + attribute_names=attrib, attribute_values=attrib_value + ) + if pxe_config: + ret["comment"] = "Pass" + ret["result"] = True + return ret + + +def system_onetime_boot_uefi(name, boot_device): + """ + This state will be used to set system one time boot to device + Params: + - boot_mode : system boot mode used for one time boot + - boot_device : device name for one time boot + + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_onetime_boot boot_mode="Bios" boot_device="Pxe" + + """ + + ret = {"name": name, "result": False, "comment": "Wrong reset type", "changes": {}} + if __opts__["test"]: + ret["comment"] = "OneTime boot set to device {0}".format(boot_device) + ret["result"] = None + return ret + status = __salt__["redfish.system_onetime_boot_device_uefi"](boot_device) + if status: + ret["result"] = True + ret["comment"] = "set One Time Boot to {boot_device}".format( + boot_device=boot_device + ) + return ret + else: + ret["comment"] = "ForceRestart Failed" + ret["result"] = False + return ret + + +def virtual_media_attached(name, url, force=False): + """ + set system power state + Params: + - power_state : set power state + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_power_state + + + """ + ret = { + "name": name, + "result": False, + "comment": "Virtual Media Attach faild", + "changes": {}, + } + if __opts__["test"]: + ret["comment"] = "virtual media attached {0}".format(url) + ret["result"] = None + return ret + + media = __salt__["redfish.manager_get_virtual_media_cd"]() + if "Inserted" in media: + if media["Image"] == url: + ret["result"] = True + ret["comment"] = "{url} already attached".format(url=url) + else: + media = __salt__["redfish.manager_attach_cd"](url, force) + if media: + ret["result"] = True + ret["comment"] = "{url} attached".format(url=url) + ret["changes"]["iso"] = url + return ret + + +def virtual_removable_disk_attached(name, url, force=False): + """ + set system power state + Params: + - power_state : set power state + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_power_state + + + """ + + ret = { + "name": name, + "result": False, + "comment": "Virtual Media Attach faild", + "changes": {}, + } + if __opts__["test"]: + ret["comment"] = "Removable Disk attached {0}".format(url) + ret["result"] = None + return ret + media = __salt__["redfish.manager_get_virtual_media_removable_disk"]() + if "Inserted" in media: + if media["Image"] == url: + ret["result"] = True + ret["comment"] = "{url} already attached".format(url=url) + else: + media = __salt__["redfish.manager_attach_removable_disk"](url, force) + if media: + ret["result"] = True + ret["comment"] = "{url} attached".format(url=url) + ret["changes"]["iso"] = url + return ret + + +def one_time_boot(name, media): + """ + set system power state + Params: + - power_state : set power state + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_power_state + + + """ + + ret = {"name": name, "result": False, "comment": "bios reset faild", "changes": {}} + if __opts__["test"]: + ret["comment"] = "one time boot into {0}".format(media) + ret["result"] = None + return ret + boot_options = __salt__["redfish.system_get_current_boot_devices"]() + if boot_options and media in boot_options: + onetime = __salt__["redfish.system_onetime_boot"]( + boot_mode="Uefi", boot_device="Pxe" + ) + if onetime: + reset = __salt__["redfish.system_reset"](reset_type="ForceRestart") + if reset: + ret["result"] = True + ret["changes"]["onetimeBoot"] = media + ret["comment"] = "one time booting into {media}".format(media=media) + return ret + + +def check_firmware_update(name): + """ + set system power state + Params: + - power_state : set power state + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_power_state + + + """ + + updates = {} + ret = { + "name": name, + "result": False, + "comment": "Firmware update check failed", + "changes": {}, + } + if __opts__["test"]: + ret["comment"] = "checking firmware to update" + ret["result"] = None + return ret + components = __salt__["redfish.update_service_get_fw_components"]( + comp_type="Installed" + ) + sysinfo = __salt__["redfish.get_system_info"]() + if "Model" in sysinfo: + model = sysinfo["Model"].split(" ")[1] + latest_fws = __salt__["redfish.parse_catalogue_file"](model, components) + current_fws = __salt__["redfish.get_installed_version"](components) + for i in current_fws.keys(): + if ( + salt.utils.versions.version_cmp( + latest_fws[i]["version"], current_fws[i]["version"] + ) + == 1 + ): + updates[i] = { + "name": current_fws[i]["name"], + "Current Version": current_fws[i]["version"], + "Latest Version": latest_fws[i]["version"], + "path": latest_fws[i]["path"], + } + ret["result"] = True + ret["changes"] = updates + ret["comment"] = "Firmware check is successful" + return ret + + +def firmware_update_to_latest(name): + """ + set system power state + Params: + - power_state : set power state + Return: dictonory of result + + CLI Example: + .. code-block:: bash + salt '*' redfish.system_power_state + + + """ + + ret = { + "name": name, + "result": False, + "comment": "Firmware update failed", + "changes": {}, + } + if __opts__["test"]: + ret["comment"] = "update firmware to latest" + ret["result"] = None + return ret + components = __salt__["redfish.update_service_get_fw_components"]( + comp_type="Installed" + ) + sysinfo = __salt__["redfish.get_system_info"]() + if "Model" in sysinfo: + model = sysinfo["Model"].split(" ")[1] + latest_fws = __salt__["redfish.parse_catalogue_file"](model, components) + current_fws = __salt__["redfish.get_current_version"](components) + for i in current_fws.keys(): + if ( + salt.utils.versions.version_cmp( + latest_fws[i]["version"], current_fws[i]["version"] + ) + == 1 + ): + download = __salt__["redfish.download_firmware"]( + latest_fws[i]["path"], latest_fws[i]["hashMD5"] + ) + if download["result"] and latest_fws[i]["rebootRequired"] == "false": + upload_fw = __salt__["redfish.upload_firmware"](download["path"]) + upload_fw = __salt__["redfish.update_firmware"]( + upload_fw["data"]["@odata.id"] + ) + break + ret["changes"] = upload_fw + ret["result"] = True + + return ret diff --git a/tests/pytests/unit/modules/test_redfish.py b/tests/pytests/unit/modules/test_redfish.py new file mode 100644 index 00000000000..cb173bfbab5 --- /dev/null +++ b/tests/pytests/unit/modules/test_redfish.py @@ -0,0 +1,854 @@ +import os.path +import pytest +import salt.modules.redfish as redfish +from unittest.mock import MagicMock, patch + + +@pytest.fixture(autouse=True) +def setup_loader(): + setup_loader_modules = {redfish: {"__proxy__": {}}} + with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: + yield loader_mock + + +def test_system_get_current_boot_devices(): + + ret = [ + "None", + "Pxe", + "Floppy", + "Cd", + "Hdd", + "BiosSetup", + "Utilities", + "UefiTarget", + "SDCard", + "UefiHttp", + ] + mock = MagicMock( + return_value={ + "Boot": { + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Floppy", + "Cd", + "Hdd", + "BiosSetup", + "Utilities", + "UefiTarget", + "SDCard", + "UefiHttp", + ] + } + } + ) + + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_current_boot_devices() == ret + + +def test__system_get_reset_type(): + + ret = [ + "On", + "ForceOff", + "ForceRestart", + "GracefulShutdown", + "PushPowerButton", + "Nmi", + ] + mock = MagicMock( + return_value={ + "Actions": { + "#ComputerSystem.Reset": { + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "ForceRestart", + "GracefulShutdown", + "PushPowerButton", + "Nmi", + ] + } + } + } + ) + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_reset_type() == ret + + +def test__system_get_boot_options(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/BootOptions/Boot0000" + } + ] + } + ) + + ret = { + "Boot0000": { + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/BootOptions/Boot0000" + } + ] + } + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_boot_options() == ret + + +def test__system_get_PSU_info(): + ret = { + "PSU.Slot.1": { + "Links": { + "PoweredBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power/PowerSupplies/PSU.Slot.1" + } + ] + } + } + } + mock = MagicMock( + return_value={ + "Links": { + "PoweredBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power/PowerSupplies/PSU.Slot.1" + } + ] + } + } + ) + + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_PSU_info() == ret + + +def test__system_get_chassis_cooling_devices(): + ret = { + "0x17%7C%7CFan.Embedded.1": { + "Links": { + "CooledBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Sensors/Fans/0x17%7C%7CFan.Embedded.1" + } + ] + } + } + } + mock = MagicMock( + return_value={ + "Links": { + "CooledBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Sensors/Fans/0x17%7C%7CFan.Embedded.1" + } + ] + } + } + ) + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_chassis_cooling_devices() == ret + + +def test__system_get_processor_info(): + ret = { + "CPU.Socket.1": { + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors/CPU.Socket.1" + } + ] + } + } + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors/CPU.Socket.1" + } + ] + } + ) + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_processor_info() == ret + + +def test__system_get_secureboot_info(): + ret = { + "Actions": { + "#SecureBoot.ResetKeys": { + "ResetKeysType@Redfish.AllowableValues": [ + "ResetAllKeysToDefault", + "DeleteAllKeys", + "DeletePK", + "ResetPK", + "ResetKEK", + "ResetDB", + "ResetDBX", + ] + } + } + } + mock = MagicMock( + return_value={ + "Actions": { + "#SecureBoot.ResetKeys": { + "ResetKeysType@Redfish.AllowableValues": [ + "ResetAllKeysToDefault", + "DeleteAllKeys", + "DeletePK", + "ResetPK", + "ResetKEK", + "ResetDB", + "ResetDBX", + ] + } + } + } + ) + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_secureboot_info() == ret + + +def test__system_get_storage_controllers_info(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.Slot.1-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/AHCI.Embedded.1-1" + }, + ] + } + ) + ret = { + "RAID.Slot.1-1": { + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.Slot.1-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/AHCI.Embedded.1-1" + }, + ] + }, + "AHCI.Embedded.1-1": { + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/RAID.Slot.1-1" + }, + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/Storage/AHCI.Embedded.1-1" + }, + ] + }, + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_storage_controllers_info() == ret + + +def test__get_lc_logs(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Lclog/141673", + "@odata.type": "#LogEntry.v1_3_0.LogEntry", + "Created": "2020-10-07T05:02:44-05:00", + "Description": " Log Entry 141673", + "EntryType": "Oem", + "Id": "141673", + "Links": { + "OriginOfCondition": { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" + } + }, + "Message": "Successfully logged in using root, from 10.107.68.191 and REDFISH.", + "MessageArgs": ["root", "10.107.68.191", "REDFISH"], + "MessageArgs@odata.count": 3, + "MessageId": "USR0030", + "Name": " Log Entry 141673", + "OemRecordFormat": "Dell", + "Severity": "OK", + } + ] + } + ) + + ret = { + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Lclog/141673", + "@odata.type": "#LogEntry.v1_3_0.LogEntry", + "Created": "2020-10-07T05:02:44-05:00", + "Description": " Log Entry 141673", + "EntryType": "Oem", + "Id": "141673", + "Links": { + "OriginOfCondition": { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1" + } + }, + "Message": "Successfully logged in using root, from 10.107.68.191 and REDFISH.", + "MessageArgs": ["root", "10.107.68.191", "REDFISH"], + "MessageArgs@odata.count": 3, + "MessageId": "USR0030", + "Name": " Log Entry 141673", + "OemRecordFormat": "Dell", + "Severity": "OK", + } + ] + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.get_lc_logs() == ret + + +def test__get_sel_logs(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Sel/795", + "@odata.type": "#LogEntry.v1_3_0.LogEntry", + "Created": "2020-10-06T06:29:23-05:00", + "Description": " Log Entry 795", + "EntryCode": "Deassert", + "EntryType": "SEL", + "Id": "795", + "Links": {}, + "Message": "The chassis is closed while the power is off.", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "802ff", + "Name": " Log Entry 795", + "SensorNumber": 115, + "SensorType": "Physical Chassis Security", + "Severity": "OK", + } + ] + } + ) + ret = { + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Logs/Sel/795", + "@odata.type": "#LogEntry.v1_3_0.LogEntry", + "Created": "2020-10-06T06:29:23-05:00", + "Description": " Log Entry 795", + "EntryCode": "Deassert", + "EntryType": "SEL", + "Id": "795", + "Links": {}, + "Message": "The chassis is closed while the power is off.", + "MessageArgs": [], + "MessageArgs@odata.count": 0, + "MessageId": "802ff", + "Name": " Log Entry 795", + "SensorNumber": 115, + "SensorType": "Physical Chassis Security", + "Severity": "OK", + } + ] + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.get_sel_logs() == ret + + +def test__get_fault_logs(): + mock = MagicMock(return_value={"Members": []}) + ret = {"Members": []} + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.get_fault_logs() == ret + + +def test__manager_get_virtual_media_removable_disk(): + mock = MagicMock( + return_value={ + "Actions": { + "#VirtualMedia.EjectMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/RemovableDisk/Actions/VirtualMedia.EjectMedia" + }, + "#VirtualMedia.InsertMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/RemovableDisk/Actions/VirtualMedia.InsertMedia" + }, + } + } + ) + ret = { + "Actions": { + "#VirtualMedia.EjectMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/RemovableDisk/Actions/VirtualMedia.EjectMedia" + }, + "#VirtualMedia.InsertMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/RemovableDisk/Actions/VirtualMedia.InsertMedia" + }, + } + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.manager_get_virtual_media_removable_disk() == ret + + +def test__manager_get_virtual_media_cd(): + mock = MagicMock( + return_value={ + "Actions": { + "#VirtualMedia.EjectMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/CD/Actions/VirtualMedia.EjectMedia" + }, + "#VirtualMedia.InsertMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/CD/Actions/VirtualMedia.InsertMedia" + }, + } + } + ) + + ret = { + "Actions": { + "#VirtualMedia.EjectMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/CD/Actions/VirtualMedia.EjectMedia" + }, + "#VirtualMedia.InsertMedia": { + "target": "/redfish/v1/Managers/iDRAC.Embedded.1/VirtualMedia/CD/Actions/VirtualMedia.InsertMedia" + }, + } + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.manager_get_virtual_media_cd() == ret + + +def test__system_get_network_adapters(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters/NIC.Embedded.1" + } + ] + } + ) + ret = { + "NIC.Embedded.1": { + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters/NIC.Embedded.1" + } + ] + } + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_network_adapters() == ret + + +def test__system_get_network_adapter_device_functions(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters/NIC.Embedded.2/NetworkDeviceFunctions/NIC.Embedded.2-1-1" + } + ] + } + ) + + adapter = "NIC.Embedded.2" + ret = { + "NIC.Embedded.2-1-1": { + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/NetworkAdapters/NIC.Embedded.2/NetworkDeviceFunctions/NIC.Embedded.2-1-1" + } + ] + } + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_network_adapter_device_functions(adapter) == ret + + +def test__system_get_ethernet_devices(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces/NIC.Embedded.2-1-1" + } + ] + } + ) + + ret = { + "NIC.Embedded.2-1-1": { + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/System.Embedded.1/EthernetInterfaces/NIC.Embedded.2-1-1" + } + ] + } + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_ethernet_devices() == ret + + +def test__get_firmware_inventory(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Current-101548-25.5.5.0005" + } + ] + } + ) + ret = { + "Current-101548-25.5.5.0005": { + "Members": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/Current-101548-25.5.5.0005" + } + ] + } + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.get_firmware_inventory() == ret + + +def test__system_get_bios_attributes(): + mock = MagicMock( + return_value={"Attributes": {"BootMode": "Uefi", "EmbSata": "AhciMode"}} + ) + attribute_names = ["BootMode", "EmbSata"] + ret = {"Attributes": {"BootMode": "Uefi", "EmbSata": "AhciMode"}} + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_bios_attributes(attribute_names) == ret + + +def test__get_system_info(): + mock = MagicMock( + return_value={ + "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", + "@odata.id": "/redfish/v1/Systems/System.Embedded.1", + "@odata.type": "#ComputerSystem.v1_5_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "ForceRestart", + "GracefulShutdown", + "PushPowerButton", + "Nmi", + ], + "target": "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset", + } + }, + } + ) + + ret = { + "@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem", + "@odata.id": "/redfish/v1/Systems/System.Embedded.1", + "@odata.type": "#ComputerSystem.v1_5_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "ForceRestart", + "GracefulShutdown", + "PushPowerButton", + "Nmi", + ], + "target": "/redfish/v1/Systems/System.Embedded.1/Actions/ComputerSystem.Reset", + } + }, + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.get_system_info() == ret + + +def test__system_get_boot_order(): + mock = MagicMock( + return_value={ + "Boot": { + "BootOrder": [ + "Boot0000", + "Boot0007", + "Boot0009", + "Boot0002", + "Boot0005", + "Boot0006", + "Boot000A", + ] + } + } + ) + ret = [ + "Boot0000", + "Boot0007", + "Boot0009", + "Boot0002", + "Boot0005", + "Boot0006", + "Boot000A", + ] + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.system_get_boot_order() == ret + + +def test__get_chassis_info(): + mock = MagicMock( + return_value={ + "@odata.context": "/redfish/v1/$metadata#Chassis.Chassis", + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1", + "@odata.type": "#Chassis.v1_6_0.Chassis", + "Actions": { + "#Chassis.Reset": { + "ResetType@Redfish.AllowableValues": ["On", "ForceOff"], + "target": "/redfish/v1/Chassis/System.Embedded.1/Actions/Chassis.Reset", + } + }, + } + ) + + ret = { + "@odata.context": "/redfish/v1/$metadata#Chassis.Chassis", + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1", + "@odata.type": "#Chassis.v1_6_0.Chassis", + "Actions": { + "#Chassis.Reset": { + "ResetType@Redfish.AllowableValues": ["On", "ForceOff"], + "target": "/redfish/v1/Chassis/System.Embedded.1/Actions/Chassis.Reset", + } + }, + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.get_chassis_info() == ret + + +def test__chassis_get_assembly_info(): + mock = MagicMock( + return_value={ + "Assemblies": [ + { + "@odata.context": "/redfish/v1/$metadata#Assembly.Assembly", + "@odata.etag": "1601983781", + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Assembly/DIMM.Socket.A1", + "@odata.type": "#Assembly.v1_0_0.AssemblyData", + "BinaryDataURI": "null", + "Description": "DDR4 DIMM", + "EngineeringChangeLevel": "null", + "MemberId": "DIMM.Socket.A1", + "Model": "DDR4 DIMM", + "Name": "DIMM.Socket.A1#FRU", + "PartNumber": "M391A1K43BB2-CTD", + "Producer": "Samsung", + "ProductionDate": "2018-12-03T06:00:00Z", + "SKU": "null", + "SparePartNumber": "null", + "Vendor": "DELL", + "Version": "null", + } + ] + } + ) + + ret = { + "Assemblies": [ + { + "@odata.context": "/redfish/v1/$metadata#Assembly.Assembly", + "@odata.etag": "1601983781", + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Assembly/DIMM.Socket.A1", + "@odata.type": "#Assembly.v1_0_0.AssemblyData", + "BinaryDataURI": "null", + "Description": "DDR4 DIMM", + "EngineeringChangeLevel": "null", + "MemberId": "DIMM.Socket.A1", + "Model": "DDR4 DIMM", + "Name": "DIMM.Socket.A1#FRU", + "PartNumber": "M391A1K43BB2-CTD", + "Producer": "Samsung", + "ProductionDate": "2018-12-03T06:00:00Z", + "SKU": "null", + "SparePartNumber": "null", + "Vendor": "DELL", + "Version": "null", + } + ] + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.chassis_get_assembly_info() == ret + + +def test__chassis_get_cooled_by_info(): + mock = MagicMock( + return_value={ + "Links": { + "CooledBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Sensors/Fans/0x17%7C%7CFan.Embedded.1" + } + ] + } + } + ) + ret = [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Sensors/Fans/0x17%7C%7CFan.Embedded.1" + } + ] + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.chassis_get_cooled_by_info() == ret + + +def test__chassis_get_powered_by_info(): + mock = MagicMock( + return_value={ + "Links": { + "PoweredBy": [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power/PowerSupplies/PSU.Slot.1" + } + ] + } + } + ) + ret = [ + { + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power/PowerSupplies/PSU.Slot.1" + } + ] + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.chassis_get_powered_by_info() == ret + + +def test__chassis_get_power_control_info(): + mock = MagicMock( + return_value={ + "@odata.context": "/redfish/v1/$metadata#Power.Power", + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power", + "@odata.type": "#Power.v1_5_0.Power", + "Description": "Power", + "Id": "Power", + "Name": "Power", + } + ) + ret = { + "@odata.context": "/redfish/v1/$metadata#Power.Power", + "@odata.id": "/redfish/v1/Chassis/System.Embedded.1/Power", + "@odata.type": "#Power.v1_5_0.Power", + "Description": "Power", + "Id": "Power", + "Name": "Power", + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.chassis_get_power_control_info() == ret + + +def test__chassis_get_power_consumed_watts(): + mock = MagicMock(return_value={"PowerControl": [{"PowerConsumedWatts": 50}]}) + ret = 50 + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.chassis_get_power_consumed_watts() == ret + + +def test__manager_get_jobs(): + mock = MagicMock( + return_value={ + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_982691247371" + } + ] + } + ) + ret = { + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_982691247371" + } + ] + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.manager_get_jobs() == ret + + +def test__manager_get_job_detail(): + mock = MagicMock( + return_value={ + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_982691247371", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": "null", + "ActualRunningStopTime": "null", + "CompletionTime": "2020-08-24T06:38:45", + "Description": "Job Instance", + "EndTime": "null", + "Id": "JID_982691247371", + "JobState": "Completed", + "JobType": "iDRACConfiguration", + "Message": "Job successfully Completed", + } + ) + ret = { + "@odata.context": "/redfish/v1/$metadata#DellJob.DellJob", + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/Jobs/JID_982691247371", + "@odata.type": "#DellJob.v1_1_0.DellJob", + "ActualRunningStartTime": "null", + "ActualRunningStopTime": "null", + "CompletionTime": "2020-08-24T06:38:45", + "Description": "Job Instance", + "EndTime": "null", + "Id": "JID_982691247371", + "JobState": "Completed", + "JobType": "iDRACConfiguration", + "Message": "Job successfully Completed", + } + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}): + assert redfish.manager_get_job_detail(job_id="982691247371") == ret + + +def test__manager_delete_job_id(): + mock = MagicMock(return_value="123") + ret = "123" + with patch.dict(redfish.__proxy__, {"redfish.http_delete": mock}): + assert redfish.manager_delete_job_id(job_id="982691247371") == ret + + +def test__manager_clear_jobs(): + mock1 = MagicMock(return_value="JID_758251537053") + mock = MagicMock( + return_value={ + "Id": "JobQueue", + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_758251537053" + }, + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_758452592711" + }, + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_759118382708" + }, + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_759310786751" + }, + { + "@odata.id": "/redfish/v1/Managers/iDRAC.Embedded.1/Jobs/JID_759981824121" + }, + ], + } + ) + + ret = {"result": True, "message": "Job queue is cleared successfully"} + with patch.dict(redfish.__proxy__, {"redfish.http_get": mock}), patch.dict( + redfish.__proxy__, {"redfish.http_delete": mock1} + ): + assert redfish.manager_clear_jobs() == ret