diff --git a/changelogs/fragments/5145-wdc-redfish-enclosure-power-state.yml b/changelogs/fragments/5145-wdc-redfish-enclosure-power-state.yml new file mode 100644 index 0000000000..738590c194 --- /dev/null +++ b/changelogs/fragments/5145-wdc-redfish-enclosure-power-state.yml @@ -0,0 +1,2 @@ +minor_changes: + - wdc_redfish_command - add ``PowerModeLow`` and ``PowerModeNormal`` commands for ``Chassis`` category (https://github.com/ansible-collections/community.general/pull/5145). diff --git a/plugins/module_utils/wdc_redfish_utils.py b/plugins/module_utils/wdc_redfish_utils.py index 0ae22de64a..d27e02d7b7 100644 --- a/plugins/module_utils/wdc_redfish_utils.py +++ b/plugins/module_utils/wdc_redfish_utils.py @@ -32,6 +32,17 @@ class WdcRedfishUtils(RedfishUtils): UPDATE_STATUS_MESSAGE_FW_UPDATE_COMPLETED_WAITING_FOR_ACTIVATION = "FW update completed. Waiting for activation." UPDATE_STATUS_MESSAGE_FW_UPDATE_FAILED = "FW update failed." + # Dict keys for resource bodies + # Standard keys + ACTIONS = "Actions" + OEM = "Oem" + WDC = "WDC" + TARGET = "target" + + # Keys for specific operations + CHASSIS_LOCATE = "#Chassis.Locate" + CHASSIS_POWER_MODE = "#Chassis.PowerMode" + def __init__(self, creds, root_uris, @@ -409,17 +420,32 @@ class WdcRedfishUtils(RedfishUtils): @staticmethod def _get_led_locate_uri(data): """Get the LED locate URI given a resource body.""" - if "Actions" not in data: + if WdcRedfishUtils.ACTIONS not in data: return None - if "Oem" not in data["Actions"]: + if WdcRedfishUtils.OEM not in data[WdcRedfishUtils.ACTIONS]: return None - if "WDC" not in data["Actions"]["Oem"]: + if WdcRedfishUtils.WDC not in data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM]: return None - if "#Chassis.Locate" not in data["Actions"]["Oem"]["WDC"]: + if WdcRedfishUtils.CHASSIS_LOCATE not in data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM][WdcRedfishUtils.WDC]: return None - if "target" not in data["Actions"]["Oem"]["WDC"]["#Chassis.Locate"]: + if WdcRedfishUtils.TARGET not in data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM][WdcRedfishUtils.WDC][WdcRedfishUtils.CHASSIS_LOCATE]: return None - return data["Actions"]["Oem"]["WDC"]["#Chassis.Locate"]["target"] + return data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM][WdcRedfishUtils.WDC][WdcRedfishUtils.CHASSIS_LOCATE][WdcRedfishUtils.TARGET] + + @staticmethod + def _get_power_mode_uri(data): + """Get the Power Mode URI given a resource body.""" + if WdcRedfishUtils.ACTIONS not in data: + return None + if WdcRedfishUtils.OEM not in data[WdcRedfishUtils.ACTIONS]: + return None + if WdcRedfishUtils.WDC not in data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM]: + return None + if WdcRedfishUtils.CHASSIS_POWER_MODE not in data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM][WdcRedfishUtils.WDC]: + return None + if WdcRedfishUtils.TARGET not in data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM][WdcRedfishUtils.WDC][WdcRedfishUtils.CHASSIS_POWER_MODE]: + return None + return data[WdcRedfishUtils.ACTIONS][WdcRedfishUtils.OEM][WdcRedfishUtils.WDC][WdcRedfishUtils.CHASSIS_POWER_MODE][WdcRedfishUtils.TARGET] def manage_indicator_led(self, command, resource_uri): key = 'IndicatorLED' @@ -452,3 +478,43 @@ class WdcRedfishUtils(RedfishUtils): return {'ret': False, 'msg': 'Invalid command'} return result + + def manage_chassis_power_mode(self, command): + return self.manage_power_mode(command, self.chassis_uri) + + def manage_power_mode(self, command, resource_uri=None): + if resource_uri is None: + resource_uri = self.chassis_uri + + payloads = {'PowerModeNormal': 'Normal', 'PowerModeLow': 'Low'} + requested_power_mode = payloads[command] + + result = {} + response = self.get_request(self.root_uri + resource_uri) + if response['ret'] is False: + return response + result['ret'] = True + data = response['data'] + + # Make sure the response includes Oem.WDC.PowerMode, and get current power mode + power_mode = 'PowerMode' + if WdcRedfishUtils.OEM not in data or WdcRedfishUtils.WDC not in data[WdcRedfishUtils.OEM] or\ + power_mode not in data[WdcRedfishUtils.OEM][WdcRedfishUtils.WDC]: + return {'ret': False, 'msg': 'Resource does not support Oem.WDC.PowerMode'} + current_power_mode = data[WdcRedfishUtils.OEM][WdcRedfishUtils.WDC][power_mode] + if current_power_mode == requested_power_mode: + return {'ret': True, 'changed': False} + + power_mode_uri = self._get_power_mode_uri(data) + if power_mode_uri is None: + return {'ret': False, 'msg': 'Power Mode URI not found.'} + + if command in payloads.keys(): + payload = {'PowerMode': payloads[command]} + response = self.post_request(self.root_uri + power_mode_uri, payload) + if response['ret'] is False: + return response + else: + return {'ret': False, 'msg': 'Invalid command'} + + return result diff --git a/plugins/modules/remote_management/redfish/wdc_redfish_command.py b/plugins/modules/remote_management/redfish/wdc_redfish_command.py index 29d6f304ef..0b89a1ff15 100644 --- a/plugins/modules/remote_management/redfish/wdc_redfish_command.py +++ b/plugins/modules/remote_management/redfish/wdc_redfish_command.py @@ -170,6 +170,18 @@ EXAMPLES = ''' username: "{{ username }}" password: "{{ password }}" +- name: Set chassis to Low Power Mode + community.general.wdc_redfish_command: + category: Chassis + resource_id: Enclosure + command: PowerModeLow + +- name: Set chassis to Normal Power Mode + community.general.wdc_redfish_command: + category: Chassis + resource_id: Enclosure + command: PowerModeNormal + ''' RETURN = ''' @@ -191,7 +203,9 @@ CATEGORY_COMMANDS_ALL = { ], "Chassis": [ "IndicatorLedOn", - "IndicatorLedOff" + "IndicatorLedOff", + "PowerModeLow", + "PowerModeNormal", ] } @@ -304,6 +318,8 @@ def main(): for command in command_list: if command.startswith("IndicatorLed"): result = rf_utils.manage_chassis_indicator_led(command) + elif command.startswith("PowerMode"): + result = rf_utils.manage_chassis_power_mode(command) if result['ret'] is False: module.fail_json(msg=to_native(result['msg'])) diff --git a/tests/unit/plugins/modules/remote_management/wdc/test_wdc_redfish_command.py b/tests/unit/plugins/modules/remote_management/wdc/test_wdc_redfish_command.py index e33db12bf2..1b2d3d3420 100644 --- a/tests/unit/plugins/modules/remote_management/wdc/test_wdc_redfish_command.py +++ b/tests/unit/plugins/modules/remote_management/wdc/test_wdc_redfish_command.py @@ -78,9 +78,17 @@ MOCK_SUCCESSFUL_RESPONSE_CHASSIS_ENCLOSURE = { "WDC": { "#Chassis.Locate": { "target": "/Chassis.Locate" + }, + "#Chassis.PowerMode": { + "target": "/redfish/v1/Chassis/Enclosure/Actions/Chassis.PowerMode", } } } + }, + "Oem": { + "WDC": { + "PowerMode": "Normal" + } } } } @@ -237,8 +245,8 @@ def mock_get_request_enclosure_multi_tenant(*args, **kwargs): raise RuntimeError("Illegal call to get_request in test: " + args[1]) -def mock_get_request_led_indicator(*args, **kwargs): - """Mock for get_request for LED indicator tests.""" +def mock_get_request(*args, **kwargs): + """Mock for get_request for simple resource tests.""" if args[1].endswith("/redfish/v1") or args[1].endswith("/redfish/v1/"): return MOCK_SUCCESSFUL_RESPONSE_WITH_UPDATE_SERVICE_RESOURCE elif args[1].endswith("/Chassis"): @@ -253,7 +261,8 @@ def mock_post_request(*args, **kwargs): """Mock post_request with successful response.""" valid_endpoints = [ "/UpdateService.FWActivate", - "/Chassis.Locate" + "/Chassis.Locate", + "/Chassis.PowerMode", ] for endpoint in valid_endpoints: if args[1].endswith(endpoint): @@ -325,6 +334,64 @@ class TestWdcRedfishCommand(unittest.TestCase): }) module.main() + def test_module_chassis_power_mode_low(self): + """Test setting chassis power mode to low (happy path).""" + module_args = { + 'category': 'Chassis', + 'command': 'PowerModeLow', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_id': 'Enclosure', + 'baseuri': 'example.com' + } + set_module_args(module_args) + with patch.multiple("ansible_collections.community.general.plugins.module_utils.wdc_redfish_utils.WdcRedfishUtils", + get_request=mock_get_request, + post_request=mock_post_request): + with self.assertRaises(AnsibleExitJson) as ansible_exit_json: + module.main() + self.assertEqual(ACTION_WAS_SUCCESSFUL_MESSAGE, + get_exception_message(ansible_exit_json)) + self.assertTrue(is_changed(ansible_exit_json)) + + def test_module_chassis_power_mode_normal_when_already_normal(self): + """Test setting chassis power mode to normal when it already is. Verify we get changed=False.""" + module_args = { + 'category': 'Chassis', + 'command': 'PowerModeNormal', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_id': 'Enclosure', + 'baseuri': 'example.com' + } + set_module_args(module_args) + with patch.multiple("ansible_collections.community.general.plugins.module_utils.wdc_redfish_utils.WdcRedfishUtils", + get_request=mock_get_request): + with self.assertRaises(AnsibleExitJson) as ansible_exit_json: + module.main() + self.assertEqual(ACTION_WAS_SUCCESSFUL_MESSAGE, + get_exception_message(ansible_exit_json)) + self.assertFalse(is_changed(ansible_exit_json)) + + def test_module_chassis_power_mode_invalid_command(self): + """Test that we get an error when issuing an invalid PowerMode command.""" + module_args = { + 'category': 'Chassis', + 'command': 'PowerModeExtraHigh', + 'username': 'USERID', + 'password': 'PASSW0RD=21', + 'resource_id': 'Enclosure', + 'baseuri': 'example.com' + } + set_module_args(module_args) + with patch.multiple("ansible_collections.community.general.plugins.module_utils.wdc_redfish_utils.WdcRedfishUtils", + get_request=mock_get_request): + with self.assertRaises(AnsibleFailJson) as ansible_fail_json: + module.main() + expected_error_message = "Invalid Command 'PowerModeExtraHigh'" + self.assertIn(expected_error_message, + get_exception_message(ansible_fail_json)) + def test_module_enclosure_led_indicator_on(self): """Test turning on a valid LED indicator (in this case we use the Enclosure resource).""" module_args = { @@ -338,7 +405,7 @@ class TestWdcRedfishCommand(unittest.TestCase): set_module_args(module_args) with patch.multiple("ansible_collections.community.general.plugins.module_utils.wdc_redfish_utils.WdcRedfishUtils", - get_request=mock_get_request_led_indicator, + get_request=mock_get_request, post_request=mock_post_request): with self.assertRaises(AnsibleExitJson) as ansible_exit_json: module.main() @@ -359,7 +426,7 @@ class TestWdcRedfishCommand(unittest.TestCase): set_module_args(module_args) with patch.multiple("ansible_collections.community.general.plugins.module_utils.wdc_redfish_utils.WdcRedfishUtils", - get_request=mock_get_request_led_indicator, + get_request=mock_get_request, post_request=mock_post_request): with self.assertRaises(AnsibleFailJson) as ansible_fail_json: module.main() @@ -380,7 +447,7 @@ class TestWdcRedfishCommand(unittest.TestCase): set_module_args(module_args) with patch.multiple("ansible_collections.community.general.plugins.module_utils.wdc_redfish_utils.WdcRedfishUtils", - get_request=mock_get_request_led_indicator): + get_request=mock_get_request): with self.assertRaises(AnsibleExitJson) as ansible_exit_json: module.main() self.assertEqual(ACTION_WAS_SUCCESSFUL_MESSAGE,