Repository: ansible/ansible
Pull request: Add GetSupportedFirmwareUpdateMethods to redfish_facts/Update commands
##### SUMMARY
<!--- Describe the change below, including rationale and design decisions -->
This pull request implements GetSupportedFirmwareUpdateMethods, which returns as a list the supported firmware update methods as shown in the UpdateService schema. 

<!--- HINT: Include "Fixes #nnn" if you are fixing an existing issue -->
Fixes #54234 

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
- Feature Pull Request

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below -->
redfish_facts
redfish_utils

##### ADDITIONAL INFORMATION
<!--- Include additional information to help people understand the change here -->
<!--- A step-by-step reproduction of the problem is helpful if there is no related issue -->

```yaml
- name: Test Redfish modules
  hosts: localhost
  gather_facts: false
  vars:
    baseuri: 10.243.5.31
    user: USERID
    password: PASSW0RD

  tasks:

    - name: Get all information available in Update category
      redfish_facts:
        category: Update
        command: GetSupportedFirmwareUpdateMethods
        baseuri: "{{ baseuri }}"
        username: "{{ user }}"
        password: "{{ password }}"
```


<!--- Paste verbatim command output below, e.g. before and after your change -->
```paste below
 ansible-playbook 2.8.0.dev0
  config file = None
  configured module search path = [u'/Users/xandermadsen/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/xandermadsen/ansible/lib/ansible
  executable location = /Users/xandermadsen/ansible/bin/ansible-playbook
  python version = 2.7.15 (default, Feb 12 2019, 11:00:12) [GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)]
No config file found; using defaults
host_list declined parsing /etc/ansible/hosts as it did not pass it's verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
script declined parsing /etc/ansible/hosts as it did not pass it's verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass it's verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
yaml declined parsing /etc/ansible/hosts as it did not pass it's verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
ini declined parsing /etc/ansible/hosts as it did not pass it's verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
toml declined parsing /etc/ansible/hosts as it did not pass it's verify_file() method
 [WARNING]: No inventory was parsed, only implicit localhost is available

 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'


PLAYBOOK: test_redfish_facts.yml **************************************************************************************************************************************************************************
1 plays in ../test_redfish_facts.yml

PLAY [Test Redfish modules] *******************************************************************************************************************************************************************************
META: ran handlers

TASK [Get all information available in Update category] ***************************************************************************************************************************************************
task path: /Users/xandermadsen/test_redfish_facts.yml:11
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: xandermadsen
<127.0.0.1> EXEC /bin/sh -c 'echo ~xandermadsen && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /Users/xandermadsen/.ansible/tmp/ansible-tmp-1553292386.15-246599246160697 `" && echo ansible-tmp-1553292386.15-246599246160697="` echo /Users/xandermadsen/.ansible/tmp/ansible-tmp-1553292386.15-246599246160697 `" ) && sleep 0'
Using module file /Users/xandermadsen/ansible/lib/ansible/modules/remote_management/redfish/redfish_facts.py
<127.0.0.1> PUT /Users/xandermadsen/.ansible/tmp/ansible-local-419185AEjdJ/tmpDvBITr TO /Users/xandermadsen/.ansible/tmp/ansible-tmp-1553292386.15-246599246160697/AnsiballZ_redfish_facts.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /Users/xandermadsen/.ansible/tmp/ansible-tmp-1553292386.15-246599246160697/ /Users/xandermadsen/.ansible/tmp/ansible-tmp-1553292386.15-246599246160697/AnsiballZ_redfish_facts.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/local/opt/python@2/bin/python2.7 /Users/xandermadsen/.ansible/tmp/ansible-tmp-1553292386.15-246599246160697/AnsiballZ_redfish_facts.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /Users/xandermadsen/.ansible/tmp/ansible-tmp-1553292386.15-246599246160697/ > /dev/null 2>&1 && sleep 0'
ok: [localhost] => (item=GetFirmwareInventory) => {
    "ansible_facts": {
        "redfish_facts": {
            "firmware_update_methods": {
                "entries": [
                    "TFTP", 
                    "SFTP"
                ], 
                "ret": true
            }
        }
    }, 
    "changed": false, 
    "failed_when_result": false, 
    "invocation": {
        "module_args": {
            "baseuri": "10.243.5.31", 
            "category": [
                "Update"
            ], 
            "command": [
                "GetSupportedFirmwareUpdateMethods"
            ], 
            "password": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", 
            "username": "USERID"
        }
    }, 
    "item": "GetFirmwareInventory"
}
META: ran handlers
META: ran handlers

PLAY RECAP ************************************************************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

```


Problem statement:
redfish_facts - Update - GetSupportedFirmwareUpdateMethods
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
This feature would implement a GetSupportedFirmwareUpdateMethods (or something less verbose) command for the Update category of redfish_facts, and would retrieve a list of the methods by which the system supports performing firmware updates.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
redfish_facts

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- HINT: You can also paste gist.github.com links for larger files -->

Hints from issue discussion:
@samerhaj
cc @billdodd @jose-delarosa @mraineri @tomasg2012
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Commit message: fix overindent

Patch to review:
diff --git a/lib/ansible/module_utils/redfish_utils.py b/lib/ansible/module_utils/redfish_utils.py
index c32b57bb4c6f7f..cce4a191250c29 100644
--- a/lib/ansible/module_utils/redfish_utils.py
+++ b/lib/ansible/module_utils/redfish_utils.py
@@ -511,6 +511,42 @@ def update_user_password(self, user):
             return response
         return {'ret': True}
 
+    def get_firmware_update_capabilities(self):
+        result = {}
+        response = self.get_request(self.root_uri + "/redfish/v1")
+        if response['ret'] is False:
+            return response
+
+        result['ret'] = True
+
+        result['entries'] = {}
+
+        data = response['data']
+        if "UpdateService" in data:
+            updateservce_uri = data['UpdateService']["@odata.id"]
+            response = self.get_request(self.root_uri + updateservce_uri)
+
+            data = response['data']
+
+            if "Actions" in data:
+                actions = data['Actions']
+                if len(actions) > 0:
+                    for key in actions.keys():
+                        action = actions.get(key)
+                        if 'title' in action:
+                            title = action['title']
+                        else:
+                            title = key
+                        result['entries'][title] = action.get('TransferProtocol@Redfish.AllowableValues',
+                                                              ["Key TransferProtocol@Redfish.AllowableValues not found"])
+                else:
+                    return {'ret': "False", 'msg': "Actions list is empty."}
+            else:
+                return {'ret': "False", 'msg': "Key Actions not found."}
+        else:
+            return {'ret': "False", 'msg': "Key UpdateService not found."}
+        return result
+
     def get_firmware_inventory(self):
         result = {}
         response = self.get_request(self.root_uri + self.firmware_uri)
@@ -605,16 +641,14 @@ def get_boot_order(self):
         boot_options_dict = {}
         for member in members:
             if '@odata.id' not in member:
-                return {'ret': False,
-                        'msg': "@odata.id not found in BootOptions"}
+                return {'ret': False, 'msg': "@odata.id not found in BootOptions"}
             boot_option_uri = member['@odata.id']
             response = self.get_request(self.root_uri + boot_option_uri)
             if response['ret'] is False:
                 return response
             data = response['data']
             if 'BootOptionReference' not in data:
-                return {'ret': False,
-                        'msg': "BootOptionReference not found in BootOption"}
+                return {'ret': False, 'msg': "BootOptionReference not found in BootOption"}
             boot_option_ref = data['BootOptionReference']
 
             # fetch the props to display for this boot device
diff --git a/lib/ansible/modules/remote_management/redfish/redfish_facts.py b/lib/ansible/modules/remote_management/redfish/redfish_facts.py
index d37e1e566a2737..c4f9807e98888e 100644
--- a/lib/ansible/modules/remote_management/redfish/redfish_facts.py
+++ b/lib/ansible/modules/remote_management/redfish/redfish_facts.py
@@ -108,6 +108,14 @@
       username: "{{ username }}"
       password: "{{ password }}"
 
+  - name: Get firmware update capability information
+    redfish_facts:
+      category: Update
+      command: GetFirmwareUpdateCapabilities
+      baseuri: "{{ baseuri }}"
+      username: "{{ username }}"
+      password: "{{ password }}"
+
   - name: Get all information available in all categories
     redfish_facts:
       category: all
@@ -134,7 +142,7 @@
                 "GetDiskInventory", "GetBiosAttributes", "GetBootOrder"],
     "Chassis": ["GetFanInventory"],
     "Accounts": ["ListUsers"],
-    "Update": ["GetFirmwareInventory"],
+    "Update": ["GetFirmwareInventory", "GetFirmwareUpdateCapabilities"],
     "Manager": ["GetManagerNicInventory", "GetLogs"],
 }
 
@@ -255,6 +263,8 @@ def main():
             for command in command_list:
                 if command == "GetFirmwareInventory":
                     result["firmware"] = rf_utils.get_firmware_inventory()
+                elif command == "GetFirmwareUpdateCapabilities":
+                    result["firmware_update_capabilities"] = rf_utils.get_firmware_update_capabilities()
 
         elif category == "Manager":
             # execute only if we find a Manager service resource


Code review comments:
diff_hunk: @@ -511,6 +511,42 @@ def update_user_password(self, user):
             return response
         return {'ret': True}
 
+    def get_firmware_update_capabilities(self):
+        result = {}
+        response = self.get_request(self.root_uri + "/redfish/v1")
path: lib/ansible/module_utils/redfish_utils.py
text: @user1:
Is it possible to not use `"/redfish/v1"` here but instead pass it from the module like we do with most other methods?

@user2:
Good catch on the `"/redfish/v1"`. I agree.