mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Redfish: Add MultipartHTTPPushUpdate (#6612)
* Redfish: Add MultipartHTTPPushUpdate Signed-off-by: Mike Raineri <michael.raineri@dell.com> * Updates based on CI results Signed-off-by: Mike Raineri <michael.raineri@dell.com> * Update plugins/modules/redfish_command.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update changelogs/fragments/6471-redfish-add-multipart-http-push-command.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/modules/redfish_command.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/module_utils/redfish_utils.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/module_utils/redfish_utils.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/module_utils/redfish_utils.py Co-authored-by: Felix Fontein <felix@fontein.de> * Update plugins/module_utils/redfish_utils.py Co-authored-by: Felix Fontein <felix@fontein.de> * Added missing import statement Signed-off-by: Mike Raineri <michael.raineri@dell.com> * Added documentation for the usage of 'timeout' Signed-off-by: Mike Raineri <michael.raineri@dell.com> --------- Signed-off-by: Mike Raineri <michael.raineri@dell.com> Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
36e8653cf7
commit
c4e7a943c0
3 changed files with 161 additions and 3 deletions
|
@ -0,0 +1,2 @@
|
|||
minor_changes:
|
||||
- redfish_command - add ``MultipartHTTPPushUpdate`` command (https://github.com/ansible-collections/community.general/issues/6471, https://github.com/ansible-collections/community.general/pull/6612).
|
|
@ -7,9 +7,14 @@ from __future__ import absolute_import, division, print_function
|
|||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from ansible.module_utils.urls import open_url
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.six import text_type
|
||||
from ansible.module_utils.six.moves import http_client
|
||||
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
|
@ -153,7 +158,7 @@ class RedfishUtils(object):
|
|||
'msg': "Failed GET request to '%s': '%s'" % (uri, to_text(e))}
|
||||
return {'ret': True, 'data': data, 'headers': headers, 'resp': resp}
|
||||
|
||||
def post_request(self, uri, pyld):
|
||||
def post_request(self, uri, pyld, multipart=False):
|
||||
req_headers = dict(POST_HEADERS)
|
||||
username, password, basic_auth = self._auth_params(req_headers)
|
||||
try:
|
||||
|
@ -162,7 +167,14 @@ class RedfishUtils(object):
|
|||
# header since this can cause conflicts with some services
|
||||
if self.sessions_uri is not None and uri == (self.root_uri + self.sessions_uri):
|
||||
basic_auth = False
|
||||
resp = open_url(uri, data=json.dumps(pyld),
|
||||
if multipart:
|
||||
# Multipart requests require special handling to encode the request body
|
||||
multipart_encoder = self._prepare_multipart(pyld)
|
||||
data = multipart_encoder[0]
|
||||
req_headers['content-type'] = multipart_encoder[1]
|
||||
else:
|
||||
data = json.dumps(pyld)
|
||||
resp = open_url(uri, data=data,
|
||||
headers=req_headers, method="POST",
|
||||
url_username=username, url_password=password,
|
||||
force_basic_auth=basic_auth, validate_certs=False,
|
||||
|
@ -298,6 +310,59 @@ class RedfishUtils(object):
|
|||
'msg': "Failed DELETE request to '%s': '%s'" % (uri, to_text(e))}
|
||||
return {'ret': True, 'resp': resp}
|
||||
|
||||
@staticmethod
|
||||
def _prepare_multipart(fields):
|
||||
"""Prepares a multipart body based on a set of fields provided.
|
||||
|
||||
Ideally it would have been good to use the existing 'prepare_multipart'
|
||||
found in ansible.module_utils.urls, but it takes files and encodes them
|
||||
as Base64 strings, which is not expected by Redfish services. It also
|
||||
adds escaping of certain bytes in the payload, such as inserting '\r'
|
||||
any time it finds a standlone '\n', which corrupts the image payload
|
||||
send to the service. This implementation is simplified to Redfish's
|
||||
usage and doesn't necessarily represent an exhaustive method of
|
||||
building multipart requests.
|
||||
"""
|
||||
|
||||
def write_buffer(body, line):
|
||||
# Adds to the multipart body based on the provided data type
|
||||
# At this time there is only support for strings, dictionaries, and bytes (default)
|
||||
if isinstance(line, text_type):
|
||||
body.append(to_bytes(line, encoding='utf-8'))
|
||||
elif isinstance(line, dict):
|
||||
body.append(to_bytes(json.dumps(line), encoding='utf-8'))
|
||||
else:
|
||||
body.append(line)
|
||||
return
|
||||
|
||||
# Generate a random boundary marker; may need to consider probing the
|
||||
# payload for potential conflicts in the future
|
||||
boundary = ''.join(random.choice(string.digits + string.ascii_letters) for i in range(30))
|
||||
body = []
|
||||
for form in fields:
|
||||
# Fill in the form details
|
||||
write_buffer(body, '--' + boundary)
|
||||
|
||||
# Insert the headers (Content-Disposition and Content-Type)
|
||||
if 'filename' in fields[form]:
|
||||
name = os.path.basename(fields[form]['filename']).replace('"', '\\"')
|
||||
write_buffer(body, u'Content-Disposition: form-data; name="%s"; filename="%s"' % (to_text(form), to_text(name)))
|
||||
else:
|
||||
write_buffer(body, 'Content-Disposition: form-data; name="%s"' % form)
|
||||
write_buffer(body, 'Content-Type: %s' % fields[form]['mime_type'])
|
||||
write_buffer(body, '')
|
||||
|
||||
# Insert the payload; read from the file if not given by the caller
|
||||
if 'content' not in fields[form]:
|
||||
with open(to_bytes(fields[form]['filename'], errors='surrogate_or_strict'), 'rb') as f:
|
||||
fields[form]['content'] = f.read()
|
||||
write_buffer(body, fields[form]['content'])
|
||||
|
||||
# Finalize the entire request
|
||||
write_buffer(body, '--' + boundary + '--')
|
||||
write_buffer(body, '')
|
||||
return (b'\r\n'.join(body), 'multipart/form-data; boundary=' + boundary)
|
||||
|
||||
@staticmethod
|
||||
def _get_extended_message(error):
|
||||
"""
|
||||
|
@ -1572,6 +1637,61 @@ class RedfishUtils(object):
|
|||
'msg': "SimpleUpdate requested",
|
||||
'update_status': self._operation_results(response['resp'], response['data'])}
|
||||
|
||||
def multipath_http_push_update(self, update_opts):
|
||||
"""
|
||||
Provides a software update via the URI specified by the
|
||||
MultipartHttpPushUri property. Callers should adjust the 'timeout'
|
||||
variable in the base object to accommodate the size of the image and
|
||||
speed of the transfer. For example, a 200MB image will likely take
|
||||
more than the default 10 second timeout.
|
||||
|
||||
:param update_opts: The parameters for the update operation
|
||||
:return: dict containing the response of the update request
|
||||
"""
|
||||
image_file = update_opts.get('update_image_file')
|
||||
targets = update_opts.get('update_targets')
|
||||
apply_time = update_opts.get('update_apply_time')
|
||||
|
||||
# Ensure the image file is provided
|
||||
if not image_file:
|
||||
return {'ret': False, 'msg':
|
||||
'Must specify update_image_file for the MultipartHTTPPushUpdate command'}
|
||||
if not os.path.isfile(image_file):
|
||||
return {'ret': False, 'msg':
|
||||
'Must specify a valid file for the MultipartHTTPPushUpdate command'}
|
||||
try:
|
||||
with open(image_file, 'rb') as f:
|
||||
image_payload = f.read()
|
||||
except Exception as e:
|
||||
return {'ret': False, 'msg':
|
||||
'Could not read file %s' % image_file}
|
||||
|
||||
# Check that multipart HTTP push updates are supported
|
||||
response = self.get_request(self.root_uri + self.update_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
data = response['data']
|
||||
if 'MultipartHttpPushUri' not in data:
|
||||
return {'ret': False, 'msg': 'Service does not support MultipartHttpPushUri'}
|
||||
update_uri = data['MultipartHttpPushUri']
|
||||
|
||||
# Assemble the JSON payload portion of the request
|
||||
payload = {"@Redfish.OperationApplyTime": "Immediate"}
|
||||
if targets:
|
||||
payload["Targets"] = targets
|
||||
if apply_time:
|
||||
payload["@Redfish.OperationApplyTime"] = apply_time
|
||||
multipart_payload = {
|
||||
'UpdateParameters': {'content': json.dumps(payload), 'mime_type': 'application/json'},
|
||||
'UpdateFile': {'filename': image_file, 'content': image_payload, 'mime_type': 'application/octet-stream'}
|
||||
}
|
||||
response = self.post_request(self.root_uri + update_uri, multipart_payload, multipart=True)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
return {'ret': True, 'changed': True,
|
||||
'msg': "MultipartHTTPPushUpdate requested",
|
||||
'update_status': self._operation_results(response['resp'], response['data'])}
|
||||
|
||||
def get_update_status(self, update_handle):
|
||||
"""
|
||||
Gets the status of an update operation.
|
||||
|
|
|
@ -137,6 +137,12 @@ options:
|
|||
- URI of the image for the update.
|
||||
type: str
|
||||
version_added: '0.2.0'
|
||||
update_image_file:
|
||||
required: false
|
||||
description:
|
||||
- Filename, with optional path, of the image for the update.
|
||||
type: path
|
||||
version_added: '7.1.0'
|
||||
update_protocol:
|
||||
required: false
|
||||
description:
|
||||
|
@ -541,6 +547,30 @@ EXAMPLES = '''
|
|||
username: operator
|
||||
password: supersecretpwd
|
||||
|
||||
- name: Multipart HTTP push update; timeout is 600 seconds to allow for a
|
||||
large image transfer
|
||||
community.general.redfish_command:
|
||||
category: Update
|
||||
command: MultipartHTTPPushUpdate
|
||||
baseuri: "{{ baseuri }}"
|
||||
username: "{{ username }}"
|
||||
password: "{{ password }}"
|
||||
timeout: 600
|
||||
update_image_file: ~/images/myupdate.img
|
||||
|
||||
- name: Multipart HTTP push with additional options; timeout is 600 seconds
|
||||
to allow for a large image transfer
|
||||
community.general.redfish_command:
|
||||
category: Update
|
||||
command: MultipartHTTPPushUpdate
|
||||
baseuri: "{{ baseuri }}"
|
||||
username: "{{ username }}"
|
||||
password: "{{ password }}"
|
||||
timeout: 600
|
||||
update_image_file: ~/images/myupdate.img
|
||||
update_targets:
|
||||
- /redfish/v1/UpdateService/FirmwareInventory/BMC
|
||||
|
||||
- name: Perform requested operations to continue the update
|
||||
community.general.redfish_command:
|
||||
category: Update
|
||||
|
@ -697,7 +727,7 @@ CATEGORY_COMMANDS_ALL = {
|
|||
"Manager": ["GracefulRestart", "ClearLogs", "VirtualMediaInsert",
|
||||
"VirtualMediaEject", "PowerOn", "PowerForceOff", "PowerForceRestart",
|
||||
"PowerGracefulRestart", "PowerGracefulShutdown", "PowerReboot"],
|
||||
"Update": ["SimpleUpdate", "PerformRequestedOperations"],
|
||||
"Update": ["SimpleUpdate", "MultipartHTTPPushUpdate", "PerformRequestedOperations"],
|
||||
}
|
||||
|
||||
|
||||
|
@ -726,6 +756,7 @@ def main():
|
|||
boot_override_mode=dict(choices=['Legacy', 'UEFI']),
|
||||
resource_id=dict(),
|
||||
update_image_uri=dict(),
|
||||
update_image_file=dict(type='path'),
|
||||
update_protocol=dict(),
|
||||
update_targets=dict(type='list', elements='str', default=[]),
|
||||
update_creds=dict(
|
||||
|
@ -791,6 +822,7 @@ def main():
|
|||
# update options
|
||||
update_opts = {
|
||||
'update_image_uri': module.params['update_image_uri'],
|
||||
'update_image_file': module.params['update_image_file'],
|
||||
'update_protocol': module.params['update_protocol'],
|
||||
'update_targets': module.params['update_targets'],
|
||||
'update_creds': module.params['update_creds'],
|
||||
|
@ -940,6 +972,10 @@ def main():
|
|||
result = rf_utils.simple_update(update_opts)
|
||||
if 'update_status' in result:
|
||||
return_values['update_status'] = result['update_status']
|
||||
elif command == "MultipartHTTPPushUpdate":
|
||||
result = rf_utils.multipath_http_push_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'])
|
||||
|
||||
|
|
Loading…
Reference in a new issue