Repository: ansible-collections/community.general

Issue:
Redfish: Add support to configure Redfish Host Interface
### Summary

Using `redfish_config`, I would like to be able to configure the Redfish Host Interface. Specifically, I'd like to toggle the `InterfaceEnabled` property to enable or disable the Redfish Host Interface.

### Issue Type

Feature Idea

### Component Name

redfish_config

### Additional Information

Currently we are using the `uri` module with (some) hardcoded URIs to enable or disable the RF HI.

One example:
```
- name: Capture current redfish host interface status
  become: false
  uri:
    url: "https://{{ bmc_address }}/{{ redfish_uri }}"
    method: GET
    user: "{{ bmc_username }}"
    password: "{{ bmc_password }}"
    validate_certs: no
    force_basic_auth: yes
    timeout: "{{ default_uri_timeout }}"
  register: redfish_interface_status
  delay: "{{ default_uri_delay }}"
  retries: "{{ default_uri_retries }}"
  until: redfish_interface_status is succeeded
  changed_when: False
  vars:
    redfish_uri: "redfish/v1/Managers/1/HostInterfaces/1"
  delegate_to: 127.0.0.1

- name: Disable redfish host interface
  become: false
  uri:
    url: "https://{{ bmc_address }}/{{ redfish_uri }}"
    method: PATCH
    user: "{{ bmc_username }}"
    password: "{{ bmc_password }}"
    validate_certs: no
    force_basic_auth: yes
    timeout: "{{ default_uri_timeout }}"
    body_format: json
    body:
      InterfaceEnabled: False
  register: redfish_host_interface_disable_status
  delay: "{{ default_uri_delay }}"
  retries: "{{ default_uri_retries }}"
  until: redfish_host_interface_disable_status is succeeded
  changed_when: True
  vars:
    redfish_uri: "redfish/v1/Managers/1/HostInterfaces/1"
  delegate_to: 127.0.0.1
  when:
    - redfish_interface_status.json.InterfaceEnabled
```

I would like to replace this with a `redfish_config` command like:
```
- name: Enable HostInterface
  community.general.redfish_config:
    category: Manager
    command: SetHostInterface 
    hostinterface_config:
      InterfaceEnabled: true
    baseuri: "{{ bmc_address }}"
    username: "{{ bmc_username }}"
    password: "{{ bmc_password }}"
```

### Code of Conduct

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


Discussion:

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
@@ -2792,3 +2792,87 @@ def set_manager_nic(self, nic_addr, nic_config):
         if response['ret'] is False:
             return response
         return {'ret': True, 'changed': True, 'msg': "Modified Manager NIC"}
+
+    def set_hostinterface_attributes(self, hostinterface_config, hostinterface_id=None):
+        response = self.get_request(self.root_uri + self.manager_uri)
+        if response['ret'] is False:
+            return response
+        data = response['data']
+        if 'HostInterfaces' not in data:
+            return {'ret': False, 'msg': "HostInterfaces resource not found"}
+
+        hostinterfaces_uri = data["HostInterfaces"]["@odata.id"]
+        response = self.get_request(self.root_uri + hostinterfaces_uri)
+        if response['ret'] is False:
+            return response
+        data = response['data']
+        uris = [a.get('@odata.id') for a in data.get('Members', []) if a.get('@odata.id')]
+        # Capture list of URIs that match a specified HostInterface resource ID
+        if hostinterface_id:
+            matching_hostinterface_uris = [uri for uri in uris if hostinterface_id in uri.split('/')[-1]]
+
+        if hostinterface_id and matching_hostinterface_uris:
+            hostinterface_uri = list.pop(matching_hostinterface_uris)
+        elif hostinterface_id and not matching_hostinterface_uris:
+            return {'ret': False, 'msg': "HostInterface ID %s not present." % hostinterface_id}
+        elif len(uris) == 1:
+            hostinterface_uri = list.pop(uris)
+        else:
+            return {'ret': False, 'msg': "HostInterface ID not defined and multiple interfaces detected."}
+
+        response = self.get_request(self.root_uri + hostinterface_uri)
+        if response['ret'] is False:
+            return response
+        current_hostinterface_config = response['data']
+        payload = {}
+        for property in hostinterface_config.keys():
+            value = hostinterface_config[property]
+            if property not in current_hostinterface_config:
+                return {'ret': False, 'msg': "Property %s in hostinterface_config is invalid" % property}
+            if isinstance(value, dict):
+                if isinstance(current_hostinterface_config[property], dict):
+                    payload[property] = value
+                elif isinstance(current_hostinterface_config[property], list):
+                    payload[property] = list()
+                    payload[property].append(value)
+                else:
+                    return {'ret': False, 'msg': "Value of property %s in hostinterface_config is invalid" % property}
+            else:
+                payload[property] = value
+
+        need_change = False
+        for property in payload.keys():
+            set_value = payload[property]
+            cur_value = current_hostinterface_config[property]
+            if not isinstance(set_value, dict) and not isinstance(set_value, list):
+                if set_value != cur_value:
+                    need_change = True
+            if isinstance(set_value, dict):
+                for subprop in payload[property].keys():
+                    if subprop not in current_hostinterface_config[property]:
+                        need_change = True
+                        break
+                    sub_set_value = payload[property][subprop]
+                    sub_cur_value = current_hostinterface_config[property][subprop]
+                    if sub_set_value != sub_cur_value:
+                        need_change = True
+            if isinstance(set_value, list):
+                if len(set_value) != len(cur_value):
+                    need_change = True
+                    continue
+                for i in range(len(set_value)):
+                    for subprop in payload[property][i].keys():
+                        if subprop not in current_hostinterface_config[property][i]:
+                            need_change = True
+                            break
+                        sub_set_value = payload[property][i][subprop]
+                        sub_cur_value = current_hostinterface_config[property][i][subprop]
+                        if sub_set_value != sub_cur_value:
+                            need_change = True
+        if not need_change:
+            return {'ret': True, 'changed': False, 'msg': "Host Interface already configured"}
+
+        response = self.patch_request(self.root_uri + hostinterface_uri, payload)
+        if response['ret'] is False:
+            return response
+        return {'ret': True, 'changed': True, 'msg': "Modified Host Interface"}
diff --git a/plugins/modules/remote_management/redfish/redfish_config.py b/plugins/modules/remote_management/redfish/redfish_config.py
--- a/plugins/modules/remote_management/redfish/redfish_config.py
+++ b/plugins/modules/remote_management/redfish/redfish_config.py
@@ -100,6 +100,18 @@
     type: bool
     default: false
     version_added: 3.7.0
+  hostinterface_config:
+    required: false
+    description:
+      - Setting dict of HostInterface on OOB controller.
+    type: dict
+    version_added: '4.1.0'
+  hostinterface_id:
+    required: false
+    description:
+      - Redfish HostInterface instance ID if multiple HostInterfaces are present.
+    type: str
+    version_added: '4.1.0'
 
 author: "Jose Delarosa (@jose-delarosa)"
 '''
@@ -201,6 +213,27 @@
       baseuri: "{{ baseuri }}"
       username: "{{ username }}"
       password: "{{ password }}"
+
+  - name: Disable Host Interface
+    community.general.redfish_config:
+      category: Manager
+      command: SetHostInterface
+      hostinterface_config:
+        InterfaceEnabled: false
+      baseuri: "{{ baseuri }}"
+      username: "{{ username }}"
+      password: "{{ password }}"
+
+  - name: Enable Host Interface for HostInterface resource ID '2'
+    community.general.redfish_config:
+      category: Manager
+      command: SetHostInterface
+      hostinterface_config:
+        InterfaceEnabled: true
+      hostinterface_id: "2"
+      baseuri: "{{ baseuri }}"
+      username: "{{ username }}"
+      password: "{{ password }}"
 '''
 
 RETURN = '''
@@ -220,7 +253,7 @@
 CATEGORY_COMMANDS_ALL = {
     "Systems": ["SetBiosDefaultSettings", "SetBiosAttributes", "SetBootOrder",
                 "SetDefaultBootOrder"],
-    "Manager": ["SetNetworkProtocols", "SetManagerNic"]
+    "Manager": ["SetNetworkProtocols", "SetManagerNic", "SetHostInterface"]
 }
 
 
@@ -248,6 +281,8 @@ def main():
                 default={}
             ),
             strip_etag_quotes=dict(type='bool', default=False),
+            hostinterface_config=dict(type='dict', default={}),
+            hostinterface_id=dict(),
         ),
         required_together=[
             ('username', 'password'),
@@ -288,6 +323,12 @@ def main():
     # Etag options
     strip_etag_quotes = module.params['strip_etag_quotes']
 
+    # HostInterface config options
+    hostinterface_config = module.params['hostinterface_config']
+
+    # HostInterface instance ID
+    hostinterface_id = module.params['hostinterface_id']
+
     # Build root URI
     root_uri = "https://" + module.params['baseuri']
     rf_utils = RedfishUtils(creds, root_uri, timeout, module,
@@ -331,6 +372,8 @@ def main():
                 result = rf_utils.set_network_protocols(module.params['network_protocols'])
             elif command == "SetManagerNic":
                 result = rf_utils.set_manager_nic(nic_addr, nic_config)
+            elif command == "SetHostInterface":
+                result = rf_utils.set_hostinterface_attributes(hostinterface_config, hostinterface_id)
 
     # Return data back or fail with proper message
     if result['ret'] is True:


Test patch: