Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -404,3 +404,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 @@ -54,6 +54,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 @@ -75,8 +81,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 @@ -124,6 +128,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 @@ -142,6 +187,10 @@
"Update": [
"FWActivate",
"UpdateAndActivate"
],
"Chassis": [
"IndicatorLedOn",
"IndicatorLedOff"
]
}

Expand All @@ -163,6 +212,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 @@ -190,6 +240,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 @@ -208,7 +261,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 @@ -235,17 +288,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 @@ -48,6 +48,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 @@ -204,15 +235,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 @@ -276,6 +323,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