From d03fdc8093d4c70dfeb6481fd24d0056bf0b14e1 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 22 Feb 2025 09:20:31 +0100
Subject: [PATCH] [PR #9684/961c9b7f backport][stable-10] Ssh config other
 options (#9794)

Ssh config other options (#9684)

* Add other_options support to ssh_config module

* Changelog fragment

* Fix missing and modified stuff

* Minor changes

* Update fragment with PR URL

* Fix PEP8 issue

* Fix idempotency issue

* Update changelogs/fragments/ssh_config_add_other_options.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/ssh_config.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/ssh_config.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Incorporate suggestions

* Missed removing str conversion

* PEP8

* Update plugins/modules/ssh_config.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add fail condition, fix codestyle

* Force lower case key values only

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 961c9b7f4c064877c6db91d145a6ecc2f18f83eb)

Co-authored-by: Stephen Bradshaw <stephen.mark.bradshaw@gmail.com>
---
 .../ssh_config_add_other_options.yml          |  2 ++
 plugins/modules/ssh_config.py                 | 22 +++++++++++++++
 .../targets/ssh_config/tasks/options.yml      | 28 +++++++++++++++++++
 3 files changed, 52 insertions(+)
 create mode 100644 changelogs/fragments/ssh_config_add_other_options.yml

diff --git a/changelogs/fragments/ssh_config_add_other_options.yml b/changelogs/fragments/ssh_config_add_other_options.yml
new file mode 100644
index 0000000000..032bd2c073
--- /dev/null
+++ b/changelogs/fragments/ssh_config_add_other_options.yml
@@ -0,0 +1,2 @@
+minor_changes:
+  - ssh_config - add ``other_options`` option (https://github.com/ansible-collections/community.general/issues/8053, https://github.com/ansible-collections/community.general/pull/9684).
\ No newline at end of file
diff --git a/plugins/modules/ssh_config.py b/plugins/modules/ssh_config.py
index 8a478ffd8c..07637e8003 100644
--- a/plugins/modules/ssh_config.py
+++ b/plugins/modules/ssh_config.py
@@ -139,6 +139,13 @@ options:
       - Sets the C(DynamicForward) option.
     type: str
     version_added: 10.1.0
+  other_options:
+    description:
+      - Provides the option to specify arbitrary SSH config entry options via a dictionary.
+      - The key names must be lower case. Keys with upper case values are rejected.
+      - The values must be strings. Other values are rejected.
+    type: dict
+    version_added: 10.4.0
 requirements:
   - paramiko
 """
@@ -152,6 +159,8 @@ EXAMPLES = r"""
     identity_file: "/home/akasurde/.ssh/id_rsa"
     port: '2223'
     state: present
+    other_options:
+      serveraliveinterval: '30'
 
 - name: Delete a host from the configuration
   community.general.ssh_config:
@@ -204,6 +213,7 @@ from copy import deepcopy
 
 from ansible.module_utils.basic import AnsibleModule, missing_required_lib
 from ansible.module_utils.common.text.converters import to_native
+from ansible.module_utils.six import string_types
 from ansible_collections.community.general.plugins.module_utils._stormssh import ConfigParser, HAS_PARAMIKO, PARAMIKO_IMPORT_ERROR
 from ansible_collections.community.general.plugins.module_utils.ssh import determine_config_file
 
@@ -274,6 +284,17 @@ class SSHConfig(object):
             controlpersist=fix_bool_str(self.params.get('controlpersist')),
             dynamicforward=self.params.get('dynamicforward'),
         )
+        if self.params.get('other_options'):
+            for key, value in self.params.get('other_options').items():
+                if key.lower() != key:
+                    self.module.fail_json(msg="The other_options key {key!r} must be lower case".format(key=key))
+                if key not in args:
+                    if not isinstance(value, string_types):
+                        self.module.fail_json(msg="The other_options value provided for key {key!r} must be a string, got {type}".format(key=key,
+                                                                                                                                         type=type(value)))
+                    args[key] = value
+                else:
+                    self.module.fail_json(msg="Multiple values provided for key {key!r}".format(key=key))
 
         config_changed = False
         hosts_changed = []
@@ -361,6 +382,7 @@ def main():
             host_key_algorithms=dict(type='str', no_log=False),
             identity_file=dict(type='path'),
             identities_only=dict(type='bool'),
+            other_options=dict(type='dict'),
             port=dict(type='str'),
             proxycommand=dict(type='str', default=None),
             proxyjump=dict(type='str', default=None),
diff --git a/tests/integration/targets/ssh_config/tasks/options.yml b/tests/integration/targets/ssh_config/tasks/options.yml
index 203c782487..d342943975 100644
--- a/tests/integration/targets/ssh_config/tasks/options.yml
+++ b/tests/integration/targets/ssh_config/tasks/options.yml
@@ -22,6 +22,8 @@
     controlpath: "~/.ssh/sockets/%r@%h-%p"
     controlpersist: yes
     dynamicforward: '10080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_add
   check_mode: true
@@ -57,6 +59,8 @@
     controlpath: "~/.ssh/sockets/%r@%h-%p"
     controlpersist: yes
     dynamicforward: '10080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_add
 
@@ -81,6 +85,8 @@
     controlpath: "~/.ssh/sockets/%r@%h-%p"
     controlpersist: yes
     dynamicforward: '10080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_add_again
 
@@ -109,6 +115,7 @@
       - "'controlpath ~/.ssh/sockets/%r@%h-%p' in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist yes' in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 10080' in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' in slurp_ssh_config['content'] | b64decode"
 
 - name: Options - Update host
   community.general.ssh_config:
@@ -123,6 +130,8 @@
     controlpath: "~/.ssh/new-sockets/%r@%h-%p"
     controlpersist: "600"
     dynamicforward: '11080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_update
 
@@ -149,6 +158,8 @@
     controlpath: "~/.ssh/new-sockets/%r@%h-%p"
     controlpersist: "600"
     dynamicforward: '11080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_update
 
@@ -178,6 +189,7 @@
       - "'controlpath ~/.ssh/new-sockets/%r@%h-%p' in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist 600' in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 11080' in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' in slurp_ssh_config['content'] | b64decode"
 
 - name: Options - Ensure no update in case option exist in ssh_config file but wasn't defined in playbook
   community.general.ssh_config:
@@ -212,6 +224,7 @@
       - "'controlpath ~/.ssh/new-sockets/%r@%h-%p' in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist 600' in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 11080' in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' in slurp_ssh_config['content'] | b64decode"
 
 - name: Debug
   debug:
@@ -264,6 +277,7 @@
       - "'controlpath ~/.ssh/sockets/%r@%h-%p' not in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist yes' not in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 10080' not in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' not in slurp_ssh_config['content'] | b64decode"
 
 # Proxycommand and ProxyJump are mutually exclusive.
 # Reset ssh_config before testing options with proxyjump
@@ -286,6 +300,8 @@
     controlpath: "~/.ssh/sockets/%r@%h-%p"
     controlpersist: yes
     dynamicforward: '10080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_add
   check_mode: true
@@ -321,6 +337,8 @@
     controlpath: "~/.ssh/sockets/%r@%h-%p"
     controlpersist: yes
     dynamicforward: '10080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_add
 
@@ -345,6 +363,8 @@
     controlpath: "~/.ssh/sockets/%r@%h-%p"
     controlpersist: yes
     dynamicforward: '10080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_add_again
 
@@ -373,6 +393,7 @@
       - "'controlpath ~/.ssh/sockets/%r@%h-%p' in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist yes' in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 10080' in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' in slurp_ssh_config['content'] | b64decode"
 
 - name: Options - Update host
   community.general.ssh_config:
@@ -387,6 +408,8 @@
     controlpath: "~/.ssh/new-sockets/%r@%h-%p"
     controlpersist: "600"
     dynamicforward: '11080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_update
 
@@ -413,6 +436,8 @@
     controlpath: "~/.ssh/new-sockets/%r@%h-%p"
     controlpersist: "600"
     dynamicforward: '11080'
+    other_options:
+      serveraliveinterval: '30'
     state: present
   register: options_update
 
@@ -442,6 +467,7 @@
       - "'controlpath ~/.ssh/new-sockets/%r@%h-%p' in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist 600' in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 11080' in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' in slurp_ssh_config['content'] | b64decode"
 
 - name: Options - Ensure no update in case option exist in ssh_config file but wasn't defined in playbook
   community.general.ssh_config:
@@ -476,6 +502,7 @@
       - "'controlpath ~/.ssh/new-sockets/%r@%h-%p' in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist 600' in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 11080' in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' in slurp_ssh_config['content'] | b64decode"
 
 - name: Debug
   debug:
@@ -528,3 +555,4 @@
       - "'controlpath ~/.ssh/sockets/%r@%h-%p' not in slurp_ssh_config['content'] | b64decode"
       - "'controlpersist yes' not in slurp_ssh_config['content'] | b64decode"
       - "'dynamicforward 10080' not in slurp_ssh_config['content'] | b64decode"
+      - "'serveraliveinterval 30' not in slurp_ssh_config['content'] | b64decode"