Skip to content

Commit

Permalink
WDC Redfish support for chassis indicator LED toggling. (ansible-coll…
Browse files Browse the repository at this point in the history
…ections#5059)

* WDC Redfish support for chassis indicator LED toggling.

* Added changelog fragment.

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
  • Loading branch information
2 people authored and Dušan Markovič committed Nov 7, 2022
1 parent 7579099 commit 7845ffc
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- wdc_redfish_command - add ``IndicatorLedOn`` and ``IndicatorLedOff`` commands for ``Chassis`` category (https://github.com/ansible-collections/community.general/pull/5059).
47 changes: 47 additions & 0 deletions plugins/module_utils/wdc_redfish_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,50 @@ def _get_installed_firmware_version_of_multi_tenant_system(self,
return iom_b_firmware_version
else:
return None

@staticmethod
def _get_led_locate_uri(data):
"""Get the LED locate URI given a resource body."""
if "Actions" not in data:
return None
if "Oem" not in data["Actions"]:
return None
if "WDC" not in data["Actions"]["Oem"]:
return None
if "#Chassis.Locate" not in data["Actions"]["Oem"]["WDC"]:
return None
if "target" not in data["Actions"]["Oem"]["WDC"]["#Chassis.Locate"]:
return None
return data["Actions"]["Oem"]["WDC"]["#Chassis.Locate"]["target"]

def manage_indicator_led(self, command, resource_uri):
key = 'IndicatorLED'

payloads = {'IndicatorLedOn': 'On', 'IndicatorLedOff': 'Off'}
current_led_status_map = {'IndicatorLedOn': 'Blinking', 'IndicatorLedOff': 'Off'}

result = {}
response = self.get_request(self.root_uri + resource_uri)
if response['ret'] is False:
return response
result['ret'] = True
data = response['data']
if key not in data:
return {'ret': False, 'msg': "Key %s not found" % key}
current_led_status = data[key]
if current_led_status == current_led_status_map[command]:
return {'ret': True, 'changed': False}

led_locate_uri = self._get_led_locate_uri(data)
if led_locate_uri is None:
return {'ret': False, 'msg': 'LED locate URI not found.'}

if command in payloads.keys():
payload = {'LocateState': payloads[command]}
response = self.post_request(self.root_uri + led_locate_uri, payload)
if response['ret'] is False:
return response
else:
return {'ret': False, 'msg': 'Invalid command'}

return result
91 changes: 80 additions & 11 deletions plugins/modules/remote_management/redfish/wdc_redfish_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@
- Timeout in seconds for URL requests to OOB controller.
default: 10
type: int
resource_id:
required: false
description:
- ID of the component to modify, such as C(Enclosure), C(IOModuleAFRU), C(PowerSupplyBFRU), C(FanExternalFRU3), or C(FanInternalFRU).
type: str
version_added: 5.4.0
update_image_uri:
required: false
description:
Expand All @@ -76,8 +82,6 @@
description:
- The password for retrieving the update image.
type: str
requirements:
- dnspython (2.1.0 for Python 3, 1.16.0 for Python 2)
notes:
- In the inventory, you can specify baseuri or ioms. See the EXAMPLES section.
- ioms is a list of FQDNs for the enclosure's IOMs.
Expand Down Expand Up @@ -125,6 +129,47 @@
update_creds:
username: operator
password: supersecretpwd
- name: Turn on enclosure indicator LED
community.general.wdc_redfish_command:
category: Chassis
resource_id: Enclosure
command: IndicatorLedOn
username: "{{ username }}"
password: "{{ password }}"
- name: Turn off IOM A indicator LED
community.general.wdc_redfish_command:
category: Chassis
resource_id: IOModuleAFRU
command: IndicatorLedOff
username: "{{ username }}"
password: "{{ password }}"
- name: Turn on Power Supply B indicator LED
community.general.wdc_redfish_command:
category: Chassis
resource_id: PowerSupplyBFRU
command: IndicatorLedOn
username: "{{ username }}"
password: "{{ password }}"
- name: Turn on External Fan 3 indicator LED
community.general.wdc_redfish_command:
category: Chassis
resource_id: FanExternalFRU3
command: IndicatorLedOn
username: "{{ username }}"
password: "{{ password }}"
- name: Turn on Internal Fan indicator LED
community.general.wdc_redfish_command:
category: Chassis
resource_id: FanInternalFRU
command: IndicatorLedOn
username: "{{ username }}"
password: "{{ password }}"
'''

RETURN = '''
Expand All @@ -143,6 +188,10 @@
"Update": [
"FWActivate",
"UpdateAndActivate"
],
"Chassis": [
"IndicatorLedOn",
"IndicatorLedOff"
]
}

Expand All @@ -164,6 +213,7 @@ def main():
password=dict(no_log=True)
)
),
resource_id=dict(),
update_image_uri=dict(),
timeout=dict(type='int', default=10)
),
Expand Down Expand Up @@ -191,6 +241,9 @@ def main():
# timeout
timeout = module.params['timeout']

# Resource to modify
resource_id = module.params['resource_id']

# Check that Category is valid
if category not in CATEGORY_COMMANDS_ALL:
module.fail_json(msg=to_native("Invalid Category '%s'. Valid Categories = %s" % (category, sorted(CATEGORY_COMMANDS_ALL.keys()))))
Expand All @@ -209,7 +262,7 @@ def main():
"https://" + iom for iom in module.params['ioms']
]
rf_utils = WdcRedfishUtils(creds, root_uris, timeout, module,
resource_id=None, data_modification=True)
resource_id=resource_id, data_modification=True)

# Organize by Categories / Commands

Expand All @@ -236,17 +289,33 @@ def main():
update_opts["update_image_uri"] = module.params['update_image_uri']
result = rf_utils.update_and_activate(update_opts)

elif category == "Chassis":
result = rf_utils._find_chassis_resource()
if result['ret'] is False:
module.fail_json(msg=to_native(result['msg']))

led_commands = ["IndicatorLedOn", "IndicatorLedOff"]

# Check if more than one led_command is present
num_led_commands = sum([command in led_commands for command in command_list])
if num_led_commands > 1:
result = {'ret': False, 'msg': "Only one IndicatorLed command should be sent at a time."}
else:
del result['ret']
changed = result.get('changed', True)
session = result.get('session', dict())
module.exit_json(changed=changed,
session=session,
msg='Action was successful' if not module.check_mode else result.get(
'msg', "No action performed in check mode."
))
for command in command_list:
if command.startswith("IndicatorLed"):
result = rf_utils.manage_chassis_indicator_led(command)

if result['ret'] is False:
module.fail_json(msg=to_native(result['msg']))
else:
del result['ret']
changed = result.get('changed', True)
session = result.get('session', dict())
module.exit_json(changed=changed,
session=session,
msg='Action was successful' if not module.check_mode else result.get(
'msg', "No action performed in check mode."
))


if __name__ == '__main__':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,37 @@
"data": {
"UpdateService": {
"@odata.id": "/UpdateService"
},
"Chassis": {
"@odata.id": "/Chassis"
}
}
}

MOCK_SUCCESSFUL_RESPONSE_CHASSIS = {
"ret": True,
"data": {
"Members": [
{
"@odata.id": "/redfish/v1/Chassis/Enclosure"
}
]
}
}

MOCK_SUCCESSFUL_RESPONSE_CHASSIS_ENCLOSURE = {
"ret": True,
"data": {
"Id": "Enclosure",
"IndicatorLED": "Off",
"Actions": {
"Oem": {
"WDC": {
"#Chassis.Locate": {
"target": "/Chassis.Locate"
}
}
}
}
}
}
Expand Down Expand Up @@ -205,15 +236,31 @@ 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."""
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"):
return MOCK_SUCCESSFUL_RESPONSE_CHASSIS
elif args[1].endswith("Chassis/Enclosure"):
return MOCK_SUCCESSFUL_RESPONSE_CHASSIS_ENCLOSURE
else:
raise RuntimeError("Illegal call to get_request in test: " + args[1])


def mock_post_request(*args, **kwargs):
"""Mock post_request with successful response."""
if args[1].endswith("/UpdateService.FWActivate"):
return {
"ret": True,
"data": ACTION_WAS_SUCCESSFUL_MESSAGE
}
else:
raise RuntimeError("Illegal POST call to: " + args[1])
valid_endpoints = [
"/UpdateService.FWActivate",
"/Chassis.Locate"
]
for endpoint in valid_endpoints:
if args[1].endswith(endpoint):
return {
"ret": True,
"data": ACTION_WAS_SUCCESSFUL_MESSAGE
}
raise RuntimeError("Illegal POST call to: " + args[1])


def mock_get_firmware_inventory_version_1_2_3(*args, **kwargs):
Expand Down Expand Up @@ -277,6 +324,68 @@ def test_module_fail_when_unknown_command(self):
})
module.main()

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 = {
'category': 'Chassis',
'command': 'IndicatorLedOn',
'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_led_indicator,
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_invalid_resource_led_indicator_on(self):
"""Test turning LED on for an invalid resource id."""
module_args = {
'category': 'Chassis',
'command': 'IndicatorLedOn',
'username': 'USERID',
'password': 'PASSW0RD=21',
"resource_id": "Disk99",
"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_led_indicator,
post_request=mock_post_request):
with self.assertRaises(AnsibleFailJson) as ansible_fail_json:
module.main()
expected_error_message = "Chassis resource Disk99 not found"
self.assertEqual(expected_error_message,
get_exception_message(ansible_fail_json))

def test_module_enclosure_led_off_already_off(self):
"""Test turning LED indicator off when it's already off. Confirm changed is False and no POST occurs."""
module_args = {
'category': 'Chassis',
'command': 'IndicatorLedOff',
'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_led_indicator):
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_fw_activate_first_iom_unavailable(self):
"""Test that if the first IOM is not available, the 2nd one is used."""
ioms = [
Expand Down

0 comments on commit 7845ffc

Please sign in to comment.