mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Redfish: Expanded SimpleUpdate command to allow for users to monitor the progress of an update and perform follow-up operations (#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>
This commit is contained in:
parent
3bf3d6bff4
commit
5c1c8152ec
5 changed files with 218 additions and 8 deletions
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- redfish_command - add ``update_apply_time`` to ``SimpleUpdate`` command (https://github.com/ansible-collections/community.general/issues/3910).
|
|
@ -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).
|
|
@ -143,7 +143,7 @@ class RedfishUtils(object):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'ret': False,
|
return {'ret': False,
|
||||||
'msg': "Failed GET request to '%s': '%s'" % (uri, to_text(e))}
|
'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):
|
def post_request(self, uri, pyld):
|
||||||
req_headers = dict(POST_HEADERS)
|
req_headers = dict(POST_HEADERS)
|
||||||
|
@ -155,6 +155,11 @@ class RedfishUtils(object):
|
||||||
force_basic_auth=basic_auth, validate_certs=False,
|
force_basic_auth=basic_auth, validate_certs=False,
|
||||||
follow_redirects='all',
|
follow_redirects='all',
|
||||||
use_proxy=True, timeout=self.timeout)
|
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())
|
headers = dict((k.lower(), v) for (k, v) in resp.info().items())
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
msg = self._get_extended_message(e)
|
msg = self._get_extended_message(e)
|
||||||
|
@ -169,7 +174,7 @@ class RedfishUtils(object):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'ret': False,
|
return {'ret': False,
|
||||||
'msg': "Failed POST request to '%s': '%s'" % (uri, to_text(e))}
|
'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):
|
def patch_request(self, uri, pyld, check_pyld=False):
|
||||||
req_headers = dict(PATCH_HEADERS)
|
req_headers = dict(PATCH_HEADERS)
|
||||||
|
@ -1384,11 +1389,82 @@ class RedfishUtils(object):
|
||||||
else:
|
else:
|
||||||
return self._software_inventory(self.software_uri)
|
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):
|
def simple_update(self, update_opts):
|
||||||
image_uri = update_opts.get('update_image_uri')
|
image_uri = update_opts.get('update_image_uri')
|
||||||
protocol = update_opts.get('update_protocol')
|
protocol = update_opts.get('update_protocol')
|
||||||
targets = update_opts.get('update_targets')
|
targets = update_opts.get('update_targets')
|
||||||
creds = update_opts.get('update_creds')
|
creds = update_opts.get('update_creds')
|
||||||
|
apply_time = update_opts.get('update_apply_time')
|
||||||
|
|
||||||
if not image_uri:
|
if not image_uri:
|
||||||
return {'ret': False, 'msg':
|
return {'ret': False, 'msg':
|
||||||
|
@ -1439,11 +1515,65 @@ class RedfishUtils(object):
|
||||||
payload["Username"] = creds.get('username')
|
payload["Username"] = creds.get('username')
|
||||||
if creds.get('password'):
|
if creds.get('password'):
|
||||||
payload["Password"] = 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)
|
response = self.post_request(self.root_uri + update_uri, payload)
|
||||||
if response['ret'] is False:
|
if response['ret'] is False:
|
||||||
return response
|
return response
|
||||||
return {'ret': True, 'changed': True,
|
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):
|
def get_bios_attributes(self, systems_uri):
|
||||||
result = {}
|
result = {}
|
||||||
|
|
|
@ -161,6 +161,24 @@ options:
|
||||||
description:
|
description:
|
||||||
- Password for retrieving the update image.
|
- Password for retrieving the update image.
|
||||||
type: str
|
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:
|
virtual_media:
|
||||||
required: false
|
required: false
|
||||||
description:
|
description:
|
||||||
|
@ -508,6 +526,15 @@ EXAMPLES = '''
|
||||||
username: operator
|
username: operator
|
||||||
password: supersecretpwd
|
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
|
- name: Insert Virtual Media
|
||||||
community.general.redfish_command:
|
community.general.redfish_command:
|
||||||
category: Systems
|
category: Systems
|
||||||
|
@ -610,6 +637,20 @@ msg:
|
||||||
returned: always
|
returned: always
|
||||||
type: str
|
type: str
|
||||||
sample: "Action was successful"
|
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
|
from ansible.module_utils.basic import AnsibleModule
|
||||||
|
@ -630,12 +671,13 @@ CATEGORY_COMMANDS_ALL = {
|
||||||
"Manager": ["GracefulRestart", "ClearLogs", "VirtualMediaInsert",
|
"Manager": ["GracefulRestart", "ClearLogs", "VirtualMediaInsert",
|
||||||
"VirtualMediaEject", "PowerOn", "PowerForceOff", "PowerForceRestart",
|
"VirtualMediaEject", "PowerOn", "PowerForceOff", "PowerForceRestart",
|
||||||
"PowerGracefulRestart", "PowerGracefulShutdown", "PowerReboot"],
|
"PowerGracefulRestart", "PowerGracefulShutdown", "PowerReboot"],
|
||||||
"Update": ["SimpleUpdate"]
|
"Update": ["SimpleUpdate", "PerformRequestedOperations"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
result = {}
|
result = {}
|
||||||
|
return_values = {}
|
||||||
module = AnsibleModule(
|
module = AnsibleModule(
|
||||||
argument_spec=dict(
|
argument_spec=dict(
|
||||||
category=dict(required=True),
|
category=dict(required=True),
|
||||||
|
@ -667,6 +709,9 @@ def main():
|
||||||
password=dict(no_log=True)
|
password=dict(no_log=True)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
update_apply_time=dict(choices=['Immediate', 'OnReset', 'AtMaintenanceWindowStart',
|
||||||
|
'InMaintenanceWindowOnReset', 'OnStartUpdateRequest']),
|
||||||
|
update_handle=dict(),
|
||||||
virtual_media=dict(
|
virtual_media=dict(
|
||||||
type='dict',
|
type='dict',
|
||||||
options=dict(
|
options=dict(
|
||||||
|
@ -721,7 +766,9 @@ def main():
|
||||||
'update_image_uri': module.params['update_image_uri'],
|
'update_image_uri': module.params['update_image_uri'],
|
||||||
'update_protocol': module.params['update_protocol'],
|
'update_protocol': module.params['update_protocol'],
|
||||||
'update_targets': module.params['update_targets'],
|
'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
|
# Boot override options
|
||||||
|
@ -859,6 +906,10 @@ def main():
|
||||||
for command in command_list:
|
for command in command_list:
|
||||||
if command == "SimpleUpdate":
|
if command == "SimpleUpdate":
|
||||||
result = rf_utils.simple_update(update_opts)
|
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
|
# Return data back or fail with proper message
|
||||||
if result['ret'] is True:
|
if result['ret'] is True:
|
||||||
|
@ -866,7 +917,8 @@ def main():
|
||||||
changed = result.get('changed', True)
|
changed = result.get('changed', True)
|
||||||
session = result.get('session', dict())
|
session = result.get('session', dict())
|
||||||
module.exit_json(changed=changed, session=session,
|
module.exit_json(changed=changed, session=session,
|
||||||
msg='Action was successful')
|
msg='Action was successful',
|
||||||
|
return_values=return_values)
|
||||||
else:
|
else:
|
||||||
module.fail_json(msg=to_native(result['msg']))
|
module.fail_json(msg=to_native(result['msg']))
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,12 @@ options:
|
||||||
- Timeout in seconds for HTTP requests to OOB controller.
|
- Timeout in seconds for HTTP requests to OOB controller.
|
||||||
default: 10
|
default: 10
|
||||||
type: int
|
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)"
|
author: "Jose Delarosa (@jose-delarosa)"
|
||||||
'''
|
'''
|
||||||
|
@ -247,6 +253,15 @@ EXAMPLES = '''
|
||||||
username: "{{ username }}"
|
username: "{{ username }}"
|
||||||
password: "{{ password }}"
|
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
|
- name: Get Manager Services
|
||||||
community.general.redfish_info:
|
community.general.redfish_info:
|
||||||
category: Manager
|
category: Manager
|
||||||
|
@ -324,7 +339,8 @@ CATEGORY_COMMANDS_ALL = {
|
||||||
"GetChassisThermals", "GetChassisInventory", "GetHealthReport"],
|
"GetChassisThermals", "GetChassisInventory", "GetHealthReport"],
|
||||||
"Accounts": ["ListUsers"],
|
"Accounts": ["ListUsers"],
|
||||||
"Sessions": ["GetSessions"],
|
"Sessions": ["GetSessions"],
|
||||||
"Update": ["GetFirmwareInventory", "GetFirmwareUpdateCapabilities", "GetSoftwareInventory"],
|
"Update": ["GetFirmwareInventory", "GetFirmwareUpdateCapabilities", "GetSoftwareInventory",
|
||||||
|
"GetUpdateStatus"],
|
||||||
"Manager": ["GetManagerNicInventory", "GetVirtualMedia", "GetLogs", "GetNetworkProtocols",
|
"Manager": ["GetManagerNicInventory", "GetVirtualMedia", "GetLogs", "GetNetworkProtocols",
|
||||||
"GetHealthReport", "GetHostInterfaces", "GetManagerInventory"],
|
"GetHealthReport", "GetHostInterfaces", "GetManagerInventory"],
|
||||||
}
|
}
|
||||||
|
@ -350,7 +366,8 @@ def main():
|
||||||
username=dict(),
|
username=dict(),
|
||||||
password=dict(no_log=True),
|
password=dict(no_log=True),
|
||||||
auth_token=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=[
|
required_together=[
|
||||||
('username', 'password'),
|
('username', 'password'),
|
||||||
|
@ -372,6 +389,9 @@ def main():
|
||||||
# timeout
|
# timeout
|
||||||
timeout = module.params['timeout']
|
timeout = module.params['timeout']
|
||||||
|
|
||||||
|
# update handle
|
||||||
|
update_handle = module.params['update_handle']
|
||||||
|
|
||||||
# Build root URI
|
# Build root URI
|
||||||
root_uri = "https://" + module.params['baseuri']
|
root_uri = "https://" + module.params['baseuri']
|
||||||
rf_utils = RedfishUtils(creds, root_uri, timeout, module)
|
rf_utils = RedfishUtils(creds, root_uri, timeout, module)
|
||||||
|
@ -482,6 +502,8 @@ def main():
|
||||||
result["software"] = rf_utils.get_software_inventory()
|
result["software"] = rf_utils.get_software_inventory()
|
||||||
elif command == "GetFirmwareUpdateCapabilities":
|
elif command == "GetFirmwareUpdateCapabilities":
|
||||||
result["firmware_update_capabilities"] = rf_utils.get_firmware_update_capabilities()
|
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":
|
elif category == "Sessions":
|
||||||
# execute only if we find SessionService resources
|
# execute only if we find SessionService resources
|
||||||
|
|
Loading…
Reference in a new issue