Repository: ansible-collections/community.general

Issue:
redfish_command VirtualMediaInsert does not work with HPE iLO4
### Summary

When I attempt to use redfish_command to mount virtual media on a server with HPE iLO4 I get an error.

I used mitmproxy to look at the full response and I see that it is because `Inserted` is not a supported argument for the Patch request.
```
{
    "error": {
        "@Message.ExtendedInfo": [
            {
                "MessageArgs": [
                    "Inserted"
                ],
                "MessageId": "Base.0.10.PropertyUnknown"
            }
        ],
        "code": "iLO.0.10.ExtendedInfo",
        "message": "See @Message.ExtendedInfo for more information."
    }
}
```

Poking around on https://redfish.dmtf.org/schemas/v1/ it looks like VirtualMedia schema 1.1.0 and earlier have `Inserted` as a read-only attribute. It looks like this is breaking due to `Inserted` being forced into the request body. https://github.com/ansible-collections/community.general/blob/c3aba0f10e5d1641d77669f68ddb9173a0c8ca1d/plugins/module_utils/redfish_utils.py#L2246

~~I am going to see if I can add a check for the version contained in `@odata.type` on that line. I can submit a PR, but I don't have a way of knowing if this would break any other hardware. I would expect the hardware that requires the `Inserted` field would not be using a version of the schema where that field is designated as read-only.~~

The PR I have submitted checks the `FirmwareVersion` to see if it starts with "iLO 4" so the fix should not impact any other hardware types.

### Issue Type

Bug Report

### Component Name

redfish_command

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.5]
  config file = None
  configured module search path = ['/Users/adarobin/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/adarobin/ansible/lib/python3.10/site-packages/ansible
  ansible collection location = /Users/adarobin/.ansible/collections:/usr/share/ansible/collections
  executable location = /Users/adarobin/ansible/bin/ansible
  python version = 3.10.2 (main, Feb  2 2022, 05:51:25) [Clang 13.0.0 (clang-1300.0.29.3)]
  jinja version = 3.1.2
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general

# /Users/adarobin/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 4.8.0  

# /Users/adarobin/ansible/lib/python3.10/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.8.0  
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

_No response_

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- community.general.redfish_command:
    auth_token: '{{ iloToken }}'
    baseuri: '{{ iloAddress }}'
    category: 'Manager'
    command: 'VirtualMediaInsert'
    virtual_media:
      image_url: 'http://example.com/rhel-8.5-x86_64-boot.iso'
      media_types:
        - CD
        - DVD
      write_protected: null
      inserted: null
  delegate_to: localhost
```


### Expected Results

Virtual media is attached to the server

### Actual Results

```console (paste below)
TASK [provision_host : community.general.redfish_command] ***************************************************************************************
fatal: [redacted -> localhost]: FAILED! => {"changed": false, "msg": "HTTP Error 400 on PATCH request to 'https://10.111.250.26/redfish/v1/Managers/1/VirtualMedia/2/', extended message: 'Bad Request'"}
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/remote_management/redfish/redfish_command.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/remote_management/redfish/redfish_command.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @bhavya06 @mraineri @rajeevkallur @renxulei @tomasg2012 @xmadsen
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/module_utils/redfish_utils.py b/plugins/module_utils/redfish_utils.py
--- a/plugins/module_utils/redfish_utils.py
+++ b/plugins/module_utils/redfish_utils.py
@@ -188,7 +188,12 @@ def _get_extended_message(error):
                 body = error.read().decode('utf-8')
                 data = json.loads(body)
                 ext_info = data['error']['@Message.ExtendedInfo']
-                msg = ext_info[0]['Message']
+                # if the ExtendedInfo contains a user friendly message send it
+                # otherwise try to send the entire contents of ExtendedInfo
+                try:
+                    msg = ext_info[0]['Message']
+                except Exception:
+                    msg = str(data['error']['@Message.ExtendedInfo'])
             except Exception:
                 pass
         return msg
@@ -2236,7 +2241,7 @@ def _insert_virt_media_payload(options, param_map, data, ai):
                 payload[param] = options.get(option)
         return payload
 
-    def virtual_media_insert_via_patch(self, options, param_map, uri, data):
+    def virtual_media_insert_via_patch(self, options, param_map, uri, data, image_only=False):
         # get AllowableValues
         ai = dict((k[:-24],
                    {'AllowableValues': v}) for k, v in data.items()
@@ -2245,6 +2250,13 @@ def virtual_media_insert_via_patch(self, options, param_map, uri, data):
         payload = self._insert_virt_media_payload(options, param_map, data, ai)
         if 'Inserted' not in payload:
             payload['Inserted'] = True
+
+        # Some hardware (such as iLO 4) only supports the Image property on the PATCH operation
+        # Inserted and WriteProtected are not writable
+        if image_only:
+            del payload['Inserted']
+            del payload['WriteProtected']
+
         # PATCH the resource
         response = self.patch_request(self.root_uri + uri, payload)
         if response['ret'] is False:
@@ -2260,6 +2272,7 @@ def virtual_media_insert(self, options):
             'TransferProtocolType': 'transfer_protocol_type',
             'TransferMethod': 'transfer_method'
         }
+        image_only = False
         image_url = options.get('image_url')
         if not image_url:
             return {'ret': False,
@@ -2273,6 +2286,12 @@ def virtual_media_insert(self, options):
         data = response['data']
         if 'VirtualMedia' not in data:
             return {'ret': False, 'msg': "VirtualMedia resource not found"}
+
+        # Some hardware (such as iLO 4) only supports the Image property on the PATCH operation
+        # Inserted and WriteProtected are not writable
+        if data["FirmwareVersion"].startswith("iLO 4"):
+            image_only = True
+
         virt_media_uri = data["VirtualMedia"]["@odata.id"]
         response = self.get_request(self.root_uri + virt_media_uri)
         if response['ret'] is False:
@@ -2315,7 +2334,7 @@ def virtual_media_insert(self, options):
                             'msg': "%s action not found and PATCH not allowed"
                             % '#VirtualMedia.InsertMedia'}
             return self.virtual_media_insert_via_patch(options, param_map,
-                                                       uri, data)
+                                                       uri, data, image_only)
 
         # get the action property
         action = data['Actions']['#VirtualMedia.InsertMedia']
@@ -2334,12 +2353,18 @@ def virtual_media_insert(self, options):
             return response
         return {'ret': True, 'changed': True, 'msg': "VirtualMedia inserted"}
 
-    def virtual_media_eject_via_patch(self, uri):
+    def virtual_media_eject_via_patch(self, uri, image_only=False):
         # construct payload
         payload = {
             'Inserted': False,
             'Image': None
         }
+
+        # Some hardware (such as iLO 4) only supports the Image property on the PATCH operation
+        # Inserted is not writable
+        if image_only:
+            del payload['Inserted']
+
         # PATCH resource
         response = self.patch_request(self.root_uri + uri, payload)
         if response['ret'] is False:
@@ -2360,6 +2385,13 @@ def virtual_media_eject(self, options):
         data = response['data']
         if 'VirtualMedia' not in data:
             return {'ret': False, 'msg': "VirtualMedia resource not found"}
+
+        # Some hardware (such as iLO 4) only supports the Image property on the PATCH operation
+        # Inserted is not writable
+        image_only = False
+        if data["FirmwareVersion"].startswith("iLO 4"):
+            image_only = True
+
         virt_media_uri = data["VirtualMedia"]["@odata.id"]
         response = self.get_request(self.root_uri + virt_media_uri)
         if response['ret'] is False:
@@ -2384,7 +2416,7 @@ def virtual_media_eject(self, options):
                         return {'ret': False,
                                 'msg': "%s action not found and PATCH not allowed"
                                        % '#VirtualMedia.EjectMedia'}
-                return self.virtual_media_eject_via_patch(uri)
+                return self.virtual_media_eject_via_patch(uri, image_only)
             else:
                 # POST to the EjectMedia Action
                 action = data['Actions']['#VirtualMedia.EjectMedia']


Test patch: