From 5172941d36f52fe8e33f790caef2c1d3e575d2df Mon Sep 17 00:00:00 2001 From: Mike Moerk Date: Mon, 22 Aug 2022 09:07:32 -0600 Subject: [PATCH 1/4] WDC Redfish support for setting the power mode. --- plugins/module_utils/wdc_redfish_utils.py | 78 ++++++++++++++++-- .../redfish/wdc_redfish_command.py | 18 ++++- .../wdc/test_wdc_redfish_command.py | 79 +++++++++++++++++-- 3 files changed, 162 insertions(+), 13 deletions(-) diff --git a/plugins/module_utils/wdc_redfish_utils.py b/plugins/module_utils/wdc_redfish_utils.py index 0ae22de64a7..d27e02d7b7c 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 @@ def _get_installed_firmware_version_of_multi_tenant_system(self, @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 @@ def manage_indicator_led(self, command, resource_uri): 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 29d6f304ef4..013d9e693e5 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 @@ 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 @@ ], "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 e33db12bf2c..975bd93bd0a 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 @@ "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 @@ def test_module_fail_when_unknown_command(self): }) 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 @@ def test_module_enclosure_led_indicator_on(self): 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 @@ def test_module_invalid_resource_led_indicator_on(self): 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 @@ def test_module_enclosure_led_off_already_off(self): 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, From 6cdb97bd8cb5b2aa146bbd153bbf6193ba6cc0ec Mon Sep 17 00:00:00 2001 From: Mike Moerk Date: Mon, 22 Aug 2022 14:22:25 -0600 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Felix Fontein --- .../modules/remote_management/redfish/wdc_redfish_command.py | 2 +- .../modules/remote_management/wdc/test_wdc_redfish_command.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/remote_management/redfish/wdc_redfish_command.py b/plugins/modules/remote_management/redfish/wdc_redfish_command.py index 013d9e693e5..0b89a1ff153 100644 --- a/plugins/modules/remote_management/redfish/wdc_redfish_command.py +++ b/plugins/modules/remote_management/redfish/wdc_redfish_command.py @@ -205,7 +205,7 @@ "IndicatorLedOn", "IndicatorLedOff", "PowerModeLow", - "PowerModeNormal" + "PowerModeNormal", ] } 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 975bd93bd0a..1b2d3d34202 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 @@ -262,7 +262,7 @@ def mock_post_request(*args, **kwargs): valid_endpoints = [ "/UpdateService.FWActivate", "/Chassis.Locate", - "/Chassis.PowerMode" + "/Chassis.PowerMode", ] for endpoint in valid_endpoints: if args[1].endswith(endpoint): From b154c4b852da8cd2bf52d35fbdd6241eed16d2ba Mon Sep 17 00:00:00 2001 From: Mike Moerk Date: Mon, 29 Aug 2022 15:10:31 -0600 Subject: [PATCH 3/4] Add change fragment. --- changelogs/fragments/5145-wdc-redfish-enclosure-power-state | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/5145-wdc-redfish-enclosure-power-state diff --git a/changelogs/fragments/5145-wdc-redfish-enclosure-power-state b/changelogs/fragments/5145-wdc-redfish-enclosure-power-state new file mode 100644 index 00000000000..738590c1946 --- /dev/null +++ b/changelogs/fragments/5145-wdc-redfish-enclosure-power-state @@ -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). From d501cb55fffff9ff715dceafaf7d82b2c9500d47 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Tue, 30 Aug 2022 20:05:03 +0200 Subject: [PATCH 4/4] Add extension to changelog fragment. --- ...ure-power-state => 5145-wdc-redfish-enclosure-power-state.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelogs/fragments/{5145-wdc-redfish-enclosure-power-state => 5145-wdc-redfish-enclosure-power-state.yml} (100%) diff --git a/changelogs/fragments/5145-wdc-redfish-enclosure-power-state b/changelogs/fragments/5145-wdc-redfish-enclosure-power-state.yml similarity index 100% rename from changelogs/fragments/5145-wdc-redfish-enclosure-power-state rename to changelogs/fragments/5145-wdc-redfish-enclosure-power-state.yml