Skip to content

Commit

Permalink
Redfish: Expanded SimpleUpdate command to allow for users to monitor …
Browse files Browse the repository at this point in the history
…the progress of an update and perform follow-up operations (ansible-collections#5580)

* Redfish: Expanded SimpleUpdate command to allow for users to monitor the progress of an update and perform follow-up operations

* Update changelogs/fragments/3910-redfish-add-operation-apply-time-to-simple-update.yml

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

* Update plugins/modules/redfish_command.py

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

* Update changelogs/fragments/4276-redfish-command-updates-for-full-simple-update-workflow.yml

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

* Updated based on feedback and CI results

* Update plugins/modules/redfish_command.py

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

* Update plugins/modules/redfish_command.py

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

* Update plugins/modules/redfish_info.py

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

Co-authored-by: Felix Fontein <felix@fontein.de>
  • Loading branch information
2 people authored and russoz committed Nov 27, 2022
1 parent 6c16bda commit 5c7ff01
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- redfish_command - add ``update_apply_time`` to ``SimpleUpdate`` command (https://github.com/ansible-collections/community.general/issues/3910).
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
minor_changes:
- redfish_command - add ``update_status`` to output of ``SimpleUpdate`` command to allow a user monitor the update in progress (https://github.com/ansible-collections/community.general/issues/4276).
- redfish_info - add ``GetUpdateStatus`` command to check the progress of a previous update request (https://github.com/ansible-collections/community.general/issues/4276).
- redfish_command - add ``PerformRequestedOperations`` command to perform any operations necessary to continue the update flow (https://github.com/ansible-collections/community.general/issues/4276).
136 changes: 133 additions & 3 deletions plugins/module_utils/redfish_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def get_request(self, uri):
except Exception as e:
return {'ret': False,
'msg': "Failed GET request to '%s': '%s'" % (uri, to_text(e))}
return {'ret': True, 'data': data, 'headers': headers}
return {'ret': True, 'data': data, 'headers': headers, 'resp': resp}

def post_request(self, uri, pyld):
req_headers = dict(POST_HEADERS)
Expand All @@ -155,6 +155,11 @@ def post_request(self, uri, pyld):
force_basic_auth=basic_auth, validate_certs=False,
follow_redirects='all',
use_proxy=True, timeout=self.timeout)
try:
data = json.loads(to_native(resp.read()))
except Exception as e:
# No response data; this is okay in many cases
data = None
headers = dict((k.lower(), v) for (k, v) in resp.info().items())
except HTTPError as e:
msg = self._get_extended_message(e)
Expand All @@ -169,7 +174,7 @@ def post_request(self, uri, pyld):
except Exception as e:
return {'ret': False,
'msg': "Failed POST request to '%s': '%s'" % (uri, to_text(e))}
return {'ret': True, 'headers': headers, 'resp': resp}
return {'ret': True, 'data': data, 'headers': headers, 'resp': resp}

def patch_request(self, uri, pyld, check_pyld=False):
req_headers = dict(PATCH_HEADERS)
Expand Down Expand Up @@ -1384,11 +1389,82 @@ def get_software_inventory(self):
else:
return self._software_inventory(self.software_uri)

def _operation_results(self, response, data, handle=None):
"""
Builds the results for an operation from task, job, or action response.
:param response: HTTP response object
:param data: HTTP response data
:param handle: The task or job handle that was last used
:return: dict containing operation results
"""

operation_results = {'status': None, 'messages': [], 'handle': None, 'ret': True,
'resets_requested': []}

if response.status == 204:
# No content; successful, but nothing to return
# Use the Redfish "Completed" enum from TaskState for the operation status
operation_results['status'] = 'Completed'
else:
# Parse the response body for details

# Determine the next handle, if any
operation_results['handle'] = handle
if response.status == 202:
# Task generated; get the task monitor URI
operation_results['handle'] = response.getheader('Location', handle)

# Pull out the status and messages based on the body format
if data is not None:
response_type = data.get('@odata.type', '')
if response_type.startswith('#Task.') or response_type.startswith('#Job.'):
# Task and Job have similar enough structures to treat the same
operation_results['status'] = data.get('TaskState', data.get('JobState'))
operation_results['messages'] = data.get('Messages', [])
else:
# Error response body, which is a bit of a misnomer since it's used in successful action responses
operation_results['status'] = 'Completed'
if response.status >= 400:
operation_results['status'] = 'Exception'
operation_results['messages'] = data.get('error', {}).get('@Message.ExtendedInfo', [])
else:
# No response body (or malformed); build based on status code
operation_results['status'] = 'Completed'
if response.status == 202:
operation_results['status'] = 'New'
elif response.status >= 400:
operation_results['status'] = 'Exception'

# Clear out the handle if the operation is complete
if operation_results['status'] in ['Completed', 'Cancelled', 'Exception', 'Killed']:
operation_results['handle'] = None

# Scan the messages to see if next steps are needed
for message in operation_results['messages']:
message_id = message['MessageId']

if message_id.startswith('Update.1.') and message_id.endswith('.OperationTransitionedToJob'):
# Operation rerouted to a job; update the status and handle
operation_results['status'] = 'New'
operation_results['handle'] = message['MessageArgs'][0]
operation_results['resets_requested'] = []
# No need to process other messages in this case
break

if message_id.startswith('Base.1.') and message_id.endswith('.ResetRequired'):
# A reset to some device is needed to continue the update
reset = {'uri': message['MessageArgs'][0], 'type': message['MessageArgs'][1]}
operation_results['resets_requested'].append(reset)

return operation_results

def simple_update(self, update_opts):
image_uri = update_opts.get('update_image_uri')
protocol = update_opts.get('update_protocol')
targets = update_opts.get('update_targets')
creds = update_opts.get('update_creds')
apply_time = update_opts.get('update_apply_time')

if not image_uri:
return {'ret': False, 'msg':
Expand Down Expand Up @@ -1439,11 +1515,65 @@ def simple_update(self, update_opts):
payload["Username"] = creds.get('username')
if creds.get('password'):
payload["Password"] = creds.get('password')
if apply_time:
payload["@Redfish.OperationApplyTime"] = apply_time
response = self.post_request(self.root_uri + update_uri, payload)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True,
'msg': "SimpleUpdate requested"}
'msg': "SimpleUpdate requested",
'update_status': self._operation_results(response['resp'], response['data'])}

def get_update_status(self, update_handle):
"""
Gets the status of an update operation.
:param handle: The task or job handle tracking the update
:return: dict containing the response of the update status
"""

if not update_handle:
return {'ret': False, 'msg': 'Must provide a handle tracking the update.'}

# Get the task or job tracking the update
response = self.get_request(self.root_uri + update_handle)
if response['ret'] is False:
return response

# Inspect the response to build the update status
return self._operation_results(response['resp'], response['data'], update_handle)

def perform_requested_update_operations(self, update_handle):
"""
Performs requested operations to allow the update to continue.
:param handle: The task or job handle tracking the update
:return: dict containing the result of the operations
"""

# Get the current update status
update_status = self.get_update_status(update_handle)
if update_status['ret'] is False:
return update_status

changed = False

# Perform any requested updates
for reset in update_status['resets_requested']:
resp = self.post_request(self.root_uri + reset['uri'], {'ResetType': reset['type']})
if resp['ret'] is False:
# Override the 'changed' indicator since other resets may have
# been successful
resp['changed'] = changed
return resp
changed = True

msg = 'No operations required for the update'
if changed:
# Will need to consider finetuning this message if the scope of the
# requested operations grow over time
msg = 'One or more components reset to continue the update'
return {'ret': True, 'changed': changed, 'msg': msg}

def get_bios_attributes(self, systems_uri):
result = {}
Expand Down
58 changes: 55 additions & 3 deletions plugins/modules/redfish_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@
description:
- Password for retrieving the update image.
type: str
update_apply_time:
required: false
description:
- Time when to apply the update.
type: str
choices:
- Immediate
- OnReset
- AtMaintenanceWindowStart
- InMaintenanceWindowOnReset
- OnStartUpdateRequest
version_added: '6.1.0'
update_handle:
required: false
description:
- Handle to check the status of an update in progress.
type: str
version_added: '6.1.0'
virtual_media:
required: false
description:
Expand Down Expand Up @@ -508,6 +526,15 @@
username: operator
password: supersecretpwd
- name: Perform requested operations to continue the update
community.general.redfish_command:
category: Update
command: PerformRequestedOperations
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
update_handle: /redfish/v1/TaskService/TaskMonitors/735
- name: Insert Virtual Media
community.general.redfish_command:
category: Systems
Expand Down Expand Up @@ -610,6 +637,20 @@
returned: always
type: str
sample: "Action was successful"
return_values:
description: Dictionary containing command-specific response data from the action.
returned: on success
type: dict
version_added: 6.1.0
sample: {
"update_status": {
"handle": "/redfish/v1/TaskService/TaskMonitors/735",
"messages": [],
"resets_requested": [],
"ret": true,
"status": "New"
}
}
'''

from ansible.module_utils.basic import AnsibleModule
Expand All @@ -630,12 +671,13 @@
"Manager": ["GracefulRestart", "ClearLogs", "VirtualMediaInsert",
"VirtualMediaEject", "PowerOn", "PowerForceOff", "PowerForceRestart",
"PowerGracefulRestart", "PowerGracefulShutdown", "PowerReboot"],
"Update": ["SimpleUpdate"]
"Update": ["SimpleUpdate", "PerformRequestedOperations"],
}


def main():
result = {}
return_values = {}
module = AnsibleModule(
argument_spec=dict(
category=dict(required=True),
Expand Down Expand Up @@ -667,6 +709,9 @@ def main():
password=dict(no_log=True)
)
),
update_apply_time=dict(choices=['Immediate', 'OnReset', 'AtMaintenanceWindowStart',
'InMaintenanceWindowOnReset', 'OnStartUpdateRequest']),
update_handle=dict(),
virtual_media=dict(
type='dict',
options=dict(
Expand Down Expand Up @@ -721,7 +766,9 @@ def main():
'update_image_uri': module.params['update_image_uri'],
'update_protocol': module.params['update_protocol'],
'update_targets': module.params['update_targets'],
'update_creds': module.params['update_creds']
'update_creds': module.params['update_creds'],
'update_apply_time': module.params['update_apply_time'],
'update_handle': module.params['update_handle'],
}

# Boot override options
Expand Down Expand Up @@ -859,14 +906,19 @@ def main():
for command in command_list:
if command == "SimpleUpdate":
result = rf_utils.simple_update(update_opts)
if 'update_status' in result:
return_values['update_status'] = result['update_status']
elif command == "PerformRequestedOperations":
result = rf_utils.perform_requested_update_operations(update_opts['update_handle'])

# Return data back or fail with proper message
if result['ret'] is True:
del result['ret']
changed = result.get('changed', True)
session = result.get('session', dict())
module.exit_json(changed=changed, session=session,
msg='Action was successful')
msg='Action was successful',
return_values=return_values)
else:
module.fail_json(msg=to_native(result['msg']))

Expand Down
26 changes: 24 additions & 2 deletions plugins/modules/redfish_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
- Timeout in seconds for HTTP requests to OOB controller.
default: 10
type: int
update_handle:
required: false
description:
- Handle to check the status of an update in progress.
type: str
version_added: '6.1.0'
author: "Jose Delarosa (@jose-delarosa)"
'''
Expand Down Expand Up @@ -247,6 +253,15 @@
username: "{{ username }}"
password: "{{ password }}"
- name: Get the status of an update operation
community.general.redfish_info:
category: Update
command: GetUpdateStatus
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
update_handle: /redfish/v1/TaskService/TaskMonitors/735
- name: Get Manager Services
community.general.redfish_info:
category: Manager
Expand Down Expand Up @@ -324,7 +339,8 @@
"GetChassisThermals", "GetChassisInventory", "GetHealthReport"],
"Accounts": ["ListUsers"],
"Sessions": ["GetSessions"],
"Update": ["GetFirmwareInventory", "GetFirmwareUpdateCapabilities", "GetSoftwareInventory"],
"Update": ["GetFirmwareInventory", "GetFirmwareUpdateCapabilities", "GetSoftwareInventory",
"GetUpdateStatus"],
"Manager": ["GetManagerNicInventory", "GetVirtualMedia", "GetLogs", "GetNetworkProtocols",
"GetHealthReport", "GetHostInterfaces", "GetManagerInventory"],
}
Expand All @@ -350,7 +366,8 @@ def main():
username=dict(),
password=dict(no_log=True),
auth_token=dict(no_log=True),
timeout=dict(type='int', default=10)
timeout=dict(type='int', default=10),
update_handle=dict(),
),
required_together=[
('username', 'password'),
Expand All @@ -372,6 +389,9 @@ def main():
# timeout
timeout = module.params['timeout']

# update handle
update_handle = module.params['update_handle']

# Build root URI
root_uri = "https://" + module.params['baseuri']
rf_utils = RedfishUtils(creds, root_uri, timeout, module)
Expand Down Expand Up @@ -482,6 +502,8 @@ def main():
result["software"] = rf_utils.get_software_inventory()
elif command == "GetFirmwareUpdateCapabilities":
result["firmware_update_capabilities"] = rf_utils.get_firmware_update_capabilities()
elif command == "GetUpdateStatus":
result["update_status"] = rf_utils.get_update_status(update_handle)

elif category == "Sessions":
# execute only if we find SessionService resources
Expand Down

0 comments on commit 5c7ff01

Please sign in to comment.