1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

add Redfish VirtualMediaInsert and VirtualMediaEject commands (#494)

* add Redfish VirtualMediaInsert and VirtualMediaEject commands

* add changelog fragment
This commit is contained in:
Bill Dodd 2020-06-11 13:24:41 -05:00 committed by GitHub
parent 3044c0288f
commit 48409f6584
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 379 additions and 50 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- redfish_command - Support for virtual media insert and eject commands (https://github.com/ansible-collections/community.general/issues/493)

View file

@ -9,6 +9,7 @@ from ansible.module_utils.urls import open_url
from ansible.module_utils._text import to_text
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
GET_HEADERS = {'accept': 'application/json', 'OData-Version': '4.0'}
POST_HEADERS = {'content-type': 'application/json', 'accept': 'application/json',
@ -328,6 +329,37 @@ class RedfishUtils(object):
version='2.14')
return {'ret': True}
def _get_all_action_info_values(self, action):
"""Retrieve all parameter values for an Action from ActionInfo.
Fall back to AllowableValue annotations if no ActionInfo found.
Return the result in an ActionInfo-like dictionary, keyed
by the name of the parameter. """
ai = {}
if '@Redfish.ActionInfo' in action:
ai_uri = action['@Redfish.ActionInfo']
response = self.get_request(self.root_uri + ai_uri)
if response['ret'] is True:
data = response['data']
if 'Parameters' in data:
params = data['Parameters']
ai = dict((p['Name'], p)
for p in params if 'Name' in p)
if not ai:
ai = dict((k[:-24],
{'AllowableValues': v}) for k, v in action.items()
if k.endswith('@Redfish.AllowableValues'))
return ai
def _get_allowable_values(self, action, name, default_values=None):
if default_values is None:
default_values = []
ai = self._get_all_action_info_values(action)
allowable_values = ai.get(name, {}).get('AllowableValues')
# fallback to default values
if allowable_values is None:
allowable_values = default_values
return allowable_values
def get_logs(self):
log_svcs_uri_list = []
list_of_logs = []
@ -779,23 +811,9 @@ class RedfishUtils(object):
'msg': 'target URI missing from Action #ComputerSystem.Reset'}
action_uri = reset_action['target']
# get AllowableValues from ActionInfo
allowable_values = None
if '@Redfish.ActionInfo' in reset_action:
action_info_uri = reset_action.get('@Redfish.ActionInfo')
response = self.get_request(self.root_uri + action_info_uri)
if response['ret'] is True:
data = response['data']
if 'Parameters' in data:
params = data['Parameters']
for param in params:
if param.get('Name') == 'ResetType':
allowable_values = param.get('AllowableValues')
break
# fallback to @Redfish.AllowableValues annotation
if allowable_values is None:
allowable_values = reset_action.get('ResetType@Redfish.AllowableValues', [])
# get AllowableValues
ai = self._get_all_action_info_values(reset_action)
allowable_values = ai.get('ResetType', {}).get('AllowableValues', [])
# map ResetType to an allowable value if needed
if reset_type not in allowable_values:
@ -1255,32 +1273,6 @@ class RedfishUtils(object):
else:
return self._software_inventory(self.software_uri)
def _get_allowable_values(self, action, name, default_values=None):
if default_values is None:
default_values = []
allowable_values = None
# get Allowable values from ActionInfo
if '@Redfish.ActionInfo' in action:
action_info_uri = action.get('@Redfish.ActionInfo')
response = self.get_request(self.root_uri + action_info_uri)
if response['ret'] is True:
data = response['data']
if 'Parameters' in data:
params = data['Parameters']
for param in params:
if param.get('Name') == name:
allowable_values = param.get('AllowableValues')
break
# fallback to @Redfish.AllowableValues annotation
if allowable_values is None:
prop = '%s@Redfish.AllowableValues' % name
if prop in action:
allowable_values = action[prop]
# fallback to default values
if allowable_values is None:
allowable_values = default_values
return allowable_values
def simple_update(self, update_opts):
image_uri = update_opts.get('update_image_uri')
protocol = update_opts.get('update_protocol')
@ -2069,6 +2061,248 @@ class RedfishUtils(object):
virtualmedia['entries']))
return dict(ret=ret, entries=entries)
@staticmethod
def _find_empty_virt_media_slot(resources, media_types,
media_match_strict=True):
for uri, data in resources.items():
# check MediaTypes
if 'MediaTypes' in data and media_types:
if not set(media_types).intersection(set(data['MediaTypes'])):
continue
else:
if media_match_strict:
continue
# if ejected, 'Inserted' should be False and 'ImageName' cleared
if (not data.get('Inserted', False) and
not data.get('ImageName')):
return uri, data
return None, None
@staticmethod
def _virt_media_image_inserted(resources, image_url):
for uri, data in resources.items():
if data.get('Image'):
if urlparse(image_url) == urlparse(data.get('Image')):
if data.get('Inserted', False) and data.get('ImageName'):
return True
return False
@staticmethod
def _find_virt_media_to_eject(resources, image_url):
matched_uri, matched_data = None, None
for uri, data in resources.items():
if data.get('Image'):
if urlparse(image_url) == urlparse(data.get('Image')):
matched_uri, matched_data = uri, data
if data.get('Inserted', True) and data.get('ImageName', 'x'):
return uri, data, True
return matched_uri, matched_data, False
def _read_virt_media_resources(self, uri_list):
resources = {}
headers = {}
for uri in uri_list:
response = self.get_request(self.root_uri + uri)
if response['ret'] is False:
continue
resources[uri] = response['data']
headers[uri] = response['headers']
return resources, headers
@staticmethod
def _insert_virt_media_payload(options, param_map, data, ai):
payload = {
'Image': options.get('image_url')
}
for param, option in param_map.items():
if options.get(option) is not None and param in data:
allowable = ai.get(param, {}).get('AllowableValues', [])
if allowable and options.get(option) not in allowable:
return {'ret': False,
'msg': "Value '%s' specified for option '%s' not "
"in list of AllowableValues %s" % (
options.get(option), option,
allowable)}
payload[param] = options.get(option)
return payload
def virtual_media_insert_via_patch(self, options, param_map, uri, data):
# get AllowableValues
ai = dict((k[:-24],
{'AllowableValues': v}) for k, v in data.items()
if k.endswith('@Redfish.AllowableValues'))
# construct payload
payload = self._insert_virt_media_payload(options, param_map, data, ai)
if 'Inserted' not in payload:
payload['Inserted'] = True
# PATCH the resource
response = self.patch_request(self.root_uri + uri, payload)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True, 'msg': "VirtualMedia inserted"}
def virtual_media_insert(self, options):
param_map = {
'Inserted': 'inserted',
'WriteProtected': 'write_protected',
'UserName': 'username',
'Password': 'password',
'TransferProtocolType': 'transfer_protocol_type',
'TransferMethod': 'transfer_method'
}
image_url = options.get('image_url')
if not image_url:
return {'ret': False,
'msg': "image_url option required for VirtualMediaInsert"}
media_types = options.get('media_types')
# locate and read the VirtualMedia resources
response = self.get_request(self.root_uri + self.manager_uri)
if response['ret'] is False:
return response
data = response['data']
if 'VirtualMedia' not in data:
return {'ret': False, 'msg': "VirtualMedia resource not found"}
virt_media_uri = data["VirtualMedia"]["@odata.id"]
response = self.get_request(self.root_uri + virt_media_uri)
if response['ret'] is False:
return response
data = response['data']
virt_media_list = []
for member in data[u'Members']:
virt_media_list.append(member[u'@odata.id'])
resources, headers = self._read_virt_media_resources(virt_media_list)
# see if image already inserted; if so, nothing to do
if self._virt_media_image_inserted(resources, image_url):
return {'ret': True, 'changed': False,
'msg': "VirtualMedia '%s' already inserted" % image_url}
# find an empty slot to insert the media
# try first with strict media_type matching
uri, data = self._find_empty_virt_media_slot(
resources, media_types, media_match_strict=True)
if not uri:
# if not found, try without strict media_type matching
uri, data = self._find_empty_virt_media_slot(
resources, media_types, media_match_strict=False)
if not uri:
return {'ret': False,
'msg': "Unable to find an available VirtualMedia resource "
"%s" % ('supporting ' + str(media_types)
if media_types else '')}
# confirm InsertMedia action found
if ('Actions' not in data or
'#VirtualMedia.InsertMedia' not in data['Actions']):
# try to insert via PATCH if no InsertMedia action found
h = headers[uri]
if 'allow' in h:
methods = [m.strip() for m in h.get('allow').split(',')]
if 'PATCH' not in methods:
# if Allow header present and PATCH missing, return error
return {'ret': False,
'msg': "%s action not found and PATCH not allowed"
% '#VirtualMedia.InsertMedia'}
return self.virtual_media_insert_via_patch(options, param_map,
uri, data)
# get the action property
action = data['Actions']['#VirtualMedia.InsertMedia']
if 'target' not in action:
return {'ret': False,
'msg': "target URI missing from Action "
"#VirtualMedia.InsertMedia"}
action_uri = action['target']
# get ActionInfo or AllowableValues
ai = self._get_all_action_info_values(action)
# construct payload
payload = self._insert_virt_media_payload(options, param_map, data, ai)
# POST to action
response = self.post_request(self.root_uri + action_uri, payload)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True, 'msg': "VirtualMedia inserted"}
def virtual_media_eject_via_patch(self, uri):
# construct payload
payload = {
'Inserted': False,
'Image': None
}
# PATCH resource
response = self.patch_request(self.root_uri + uri, payload)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True,
'msg': "VirtualMedia ejected"}
def virtual_media_eject(self, options):
image_url = options.get('image_url')
if not image_url:
return {'ret': False,
'msg': "image_url option required for VirtualMediaEject"}
# locate and read the VirtualMedia resources
response = self.get_request(self.root_uri + self.manager_uri)
if response['ret'] is False:
return response
data = response['data']
if 'VirtualMedia' not in data:
return {'ret': False, 'msg': "VirtualMedia resource not found"}
virt_media_uri = data["VirtualMedia"]["@odata.id"]
response = self.get_request(self.root_uri + virt_media_uri)
if response['ret'] is False:
return response
data = response['data']
virt_media_list = []
for member in data[u'Members']:
virt_media_list.append(member[u'@odata.id'])
resources, headers = self._read_virt_media_resources(virt_media_list)
# find the VirtualMedia resource to eject
uri, data, eject = self._find_virt_media_to_eject(resources, image_url)
if uri and eject:
if ('Actions' not in data or
'#VirtualMedia.EjectMedia' not in data['Actions']):
# try to eject via PATCH if no EjectMedia action found
h = headers[uri]
if 'allow' in h:
methods = [m.strip() for m in h.get('allow').split(',')]
if 'PATCH' not in methods:
# if Allow header present and PATCH missing, return error
return {'ret': False,
'msg': "%s action not found and PATCH not allowed"
% '#VirtualMedia.EjectMedia'}
return self.virtual_media_eject_via_patch(uri)
else:
# POST to the EjectMedia Action
action = data['Actions']['#VirtualMedia.EjectMedia']
if 'target' not in action:
return {'ret': False,
'msg': "target URI property missing from Action "
"#VirtualMedia.EjectMedia"}
action_uri = action['target']
# empty payload for Eject action
payload = {}
# POST to action
response = self.post_request(self.root_uri + action_uri,
payload)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True,
'msg': "VirtualMedia ejected"}
elif uri and not eject:
# already ejected: return success but changed=False
return {'ret': True, 'changed': False,
'msg': "VirtualMedia image '%s' already ejected" %
image_url}
else:
# return failure (no resources matching image_url found)
return {'ret': False, 'changed': False,
'msg': "No VirtualMedia resource found with image '%s' "
"inserted" % image_url}
def get_psu_inventory(self):
result = {}
psu_list = []

View file

@ -135,6 +135,55 @@ options:
description:
- The password for retrieving the update image
type: str
virtual_media:
required: false
description:
- The options for VirtualMedia commands
type: dict
suboptions:
media_types:
required: false
description:
- The list of media types appropriate for the image
type: list
elements: str
image_url:
required: false
description:
- The URL od the image the insert or eject
type: str
inserted:
required: false
description:
- Indicates if the image is treated as inserted on command completion
type: bool
default: True
write_protected:
required: false
description:
- Indicates if the media is treated as write-protected
type: bool
default: True
username:
required: false
description:
- The username for accessing the image URL
type: str
password:
required: false
description:
- The password for accessing the image URL
type: str
transfer_protocol_type:
required: false
description:
- The network protocol to use with the image
type: str
transfer_method:
required: false
description:
- The transfer method to use with the image
type: str
author: "Jose Delarosa (@jose-delarosa)"
'''
@ -342,6 +391,31 @@ EXAMPLES = '''
update_creds:
username: operator
password: supersecretpwd
- name: Insert Virtual Media
redfish_command:
category: Manager
command: VirtualMediaInsert
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
virtual_media:
image_url: 'http://example.com/images/SomeLinux-current.iso'
media_types:
- CD
- DVD
resource_id: BMC
- name: Eject Virtual Media
redfish_command:
category: Manager
command: VirtualMediaEject
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
virtual_media:
image_url: 'http://example.com/images/SomeLinux-current.iso'
resource_id: BMC
'''
RETURN = '''
@ -366,7 +440,8 @@ CATEGORY_COMMANDS_ALL = {
"UpdateUserRole", "UpdateUserPassword", "UpdateUserName",
"UpdateAccountServiceProperties"],
"Sessions": ["ClearSessions"],
"Manager": ["GracefulRestart", "ClearLogs"],
"Manager": ["GracefulRestart", "ClearLogs", "VirtualMediaInsert",
"VirtualMediaEject"],
"Update": ["SimpleUpdate"]
}
@ -400,6 +475,19 @@ def main():
username=dict(),
password=dict()
)
),
virtual_media=dict(
type='dict',
options=dict(
media_types=dict(type='list', elements='str', default=[]),
image_url=dict(),
inserted=dict(type='bool', default=True),
write_protected=dict(type='bool', default=True),
username=dict(),
password=dict(no_log=True),
transfer_protocol_type=dict(),
transfer_method=dict(),
)
)
),
supports_check_mode=False
@ -434,6 +522,9 @@ def main():
'update_creds': module.params['update_creds']
}
# VirtualMedia options
virtual_media = module.params['virtual_media']
# Build root URI
root_uri = "https://" + module.params['baseuri']
rf_utils = RedfishUtils(creds, root_uri, timeout, module,
@ -512,18 +603,20 @@ def main():
result = rf_utils.clear_sessions()
elif category == "Manager":
MANAGER_COMMANDS = {
"GracefulRestart": rf_utils.restart_manager_gracefully,
"ClearLogs": rf_utils.clear_logs
}
# execute only if we find a Manager service resource
result = rf_utils._find_managers_resource()
if result['ret'] is False:
module.fail_json(msg=to_native(result['msg']))
for command in command_list:
result = MANAGER_COMMANDS[command]()
if command == 'GracefulRestart':
result = rf_utils.restart_manager_gracefully()
elif command == 'ClearLogs':
result = rf_utils.clear_logs()
elif command == 'VirtualMediaInsert':
result = rf_utils.virtual_media_insert(virtual_media)
elif command == 'VirtualMediaEject':
result = rf_utils.virtual_media_eject(virtual_media)
elif category == "Update":
# execute only if we find UpdateService resources