Repository: ansible-collections/community.general

Issue:
Redfish `GetUpdateStatus` command throws exception for a completed update if there is no response body
### Summary

In `redfish_info`, the `GetUpdateStatus` command is used to check an update handle for the progress of an update. Under the covers, this is a Redfish-defined task monitor. In most implementations, a user performing a GET on this task monitor will get back a 200 OK with a response body showing messages regarding what was updated in the system.

However, there is at least one implementation that is returning 204 No Content. Technically this is valid; a 204 simply means "everything successful, nothing to report". In this case, the underlying GET handling is throwing an exception because it's always expecting a response body and always tries to unpack data from the response.

### Issue Type

Bug Report

### Component Name

redfish_utils

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.14.9]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/rainem1/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  ansible collection location = /home/rainem1/.ansible/collections:/usr/share/ansible/collections
  executable location = /bin/ansible
  python version = 3.11.6 (main, Oct  8 2023, 05:06:43) [GCC 13.2.0] (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = True

```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
# /home/rainem1/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 8.4.0  

# /usr/lib/python3/dist-packages/ansible_collections
Collection        Version
----------------- -------
community.general 6.6.2
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
CONFIG_FILE() = /etc/ansible/ansible.cfg
```


### OS / Environment

Ubuntu 23.10

### Steps to Reproduce

Note, this requires a system that returns 204 as the final response when querying the handle referenced by the "update_handle" parameter

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
---
- hosts: all
  gather_facts: false
  vars:
    username: REDACTED
    password: REDACTED
    baseuri: REDACTED
    default_uri_timeout: 5
    default_uri_retries: 5
  tasks:
  - name: Get Update Status
    community.general.redfish_info:
      category: Update
      command: GetUpdateStatus
      baseuri: "{{ baseuri }}"
      username: "{{ username }}"
      password: "{{ password }}"
      update_handle: /redfish/v1/TaskService/Tasks/0/Monitor
    retries: "{{ default_uri_retries }}"
    register: redfish_results
    until: redfish_results.redfish_facts.update_status.status != 'Running'
  - debug:
      var: redfish_results
```


### Expected Results

Did not expect an exception to be thrown... Causes the playbook to not resolve an "status" attribute in "update_status".

### Actual Results

```console (paste below)
[2024-02-20 23:35:18] fatal: [REDACTED -> localhost]: FAILED! => {
[2024-02-20 23:35:18]     "msg": "The conditional check 'redfish_results.redfish_facts.update_status.status != 'Running'' failed. The error was: error while evaluating conditional (redfish_results.redfish_facts.update_status.status != 'Running'): 'dict object' has no attribute 'status'. 'dict object' has no attribute 'status'"
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/module_utils/redfish_utils.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/module_utils/redfish_utils.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 @jyundt @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
@@ -20,6 +20,8 @@
 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
+from ansible.module_utils.ansible_release import __version__ as ansible_version
+from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
 
 GET_HEADERS = {'accept': 'application/json', 'OData-Version': '4.0'}
 POST_HEADERS = {'content-type': 'application/json', 'accept': 'application/json',
@@ -130,7 +132,7 @@ def _check_request_payload(self, req_pyld, cur_pyld, uri):
         return resp
 
     # The following functions are to send GET/POST/PATCH/DELETE requests
-    def get_request(self, uri, override_headers=None):
+    def get_request(self, uri, override_headers=None, allow_no_resp=False):
         req_headers = dict(GET_HEADERS)
         if override_headers:
             req_headers.update(override_headers)
@@ -145,13 +147,19 @@ def get_request(self, uri, override_headers=None):
                             force_basic_auth=basic_auth, validate_certs=False,
                             follow_redirects='all',
                             use_proxy=True, timeout=self.timeout)
-            if override_headers:
-                resp = gzip.open(BytesIO(resp.read()), 'rt', encoding='utf-8')
-                data = json.loads(to_native(resp.read()))
-                headers = req_headers
-            else:
-                data = json.loads(to_native(resp.read()))
-                headers = dict((k.lower(), v) for (k, v) in resp.info().items())
+            headers = dict((k.lower(), v) for (k, v) in resp.info().items())
+            try:
+                if headers.get('content-encoding') == 'gzip' and LooseVersion(ansible_version) < LooseVersion('2.14'):
+                    # Older versions of Ansible do not automatically decompress the data
+                    # Starting in 2.14, open_url will decompress the response data by default
+                    data = json.loads(to_native(gzip.open(BytesIO(resp.read()), 'rt', encoding='utf-8').read()))
+                else:
+                    data = json.loads(to_native(resp.read()))
+            except Exception as e:
+                # No response data; this is okay in certain cases
+                data = None
+                if not allow_no_resp:
+                    raise
         except HTTPError as e:
             msg = self._get_extended_message(e)
             return {'ret': False,
@@ -1813,7 +1821,7 @@ def get_update_status(self, 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)
+        response = self.get_request(self.root_uri + update_handle, allow_no_resp=True)
         if response['ret'] is False:
             return response
 


Test patch: