1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/modules/ssh_config.py
patchback[bot] 8e84b3ef8e
[PR #6117/3862de3f backport][stable-6] Removes dependency on StormSSH (#6179)
Removes dependency on StormSSH (#6117)

* included storm config parser

* Add changelog fragment

* Fix changelog fragment

* address PR feedback

* fix license

* add required boilerplate, fix issues found in test output

* move __future__ imports

* address pr feedback

* address test output

* address pr feedback

(cherry picked from commit 3862de3f15)

Co-authored-by: Peter Upton <peterupton99@gmail.com>
2023-03-12 15:50:07 +01:00

331 lines
11 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Björn Andersson
# Copyright (c) 2021, Ansible Project
# Copyright (c) 2021, Abhijeet Kasurde <akasurde@redhat.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: ssh_config
short_description: Manage SSH config for user
version_added: '2.0.0'
description:
- Configures SSH hosts with special C(IdentityFile)s and hostnames.
author:
- Björn Andersson (@gaqzi)
- Abhijeet Kasurde (@Akasurde)
extends_documentation_fragment:
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
state:
description:
- Whether a host entry should exist or not.
default: present
choices: [ 'present', 'absent' ]
type: str
user:
description:
- Which user account this configuration file belongs to.
- If none given and I(ssh_config_file) is not specified, C(/etc/ssh/ssh_config) is used.
- If a user is given, C(~/.ssh/config) is used.
- Mutually exclusive with I(ssh_config_file).
type: str
group:
description:
- Which group this configuration file belongs to.
- If none given, I(user) is used.
type: str
host:
description:
- The endpoint this configuration is valid for.
- Can be an actual address on the internet or an alias that will
connect to the value of I(hostname).
required: true
type: str
hostname:
description:
- The actual host to connect to when connecting to the host defined.
type: str
port:
description:
- The actual port to connect to when connecting to the host defined.
type: str
remote_user:
description:
- Specifies the user to log in as.
type: str
identity_file:
description:
- The path to an identity file (SSH private key) that will be used
when connecting to this host.
- File need to exist and have mode C(0600) to be valid.
type: path
user_known_hosts_file:
description:
- Sets the user known hosts file option.
type: str
strict_host_key_checking:
description:
- Whether to strictly check the host key when doing connections to the remote host.
choices: [ 'yes', 'no', 'ask' ]
type: str
proxycommand:
description:
- Sets the C(ProxyCommand) option.
type: str
forward_agent:
description:
- Sets the C(ForwardAgent) option.
type: bool
version_added: 4.0.0
ssh_config_file:
description:
- SSH config file.
- If I(user) and this option are not specified, C(/etc/ssh/ssh_config) is used.
- Mutually exclusive with I(user).
type: path
host_key_algorithms:
description:
- Sets the C(HostKeyAlgorithms) option.
type: str
version_added: 6.1.0
requirements:
- paramiko
'''
EXAMPLES = r'''
- name: Add a host in the configuration
community.general.ssh_config:
user: akasurde
host: "example.com"
hostname: "github.com"
identity_file: "/home/akasurde/.ssh/id_rsa"
port: '2223'
state: present
- name: Delete a host from the configuration
community.general.ssh_config:
ssh_config_file: "{{ ssh_config_test }}"
host: "example.com"
state: absent
'''
RETURN = r'''
hosts_added:
description: A list of host added.
returned: success
type: list
sample: ["example.com"]
hosts_removed:
description: A list of host removed.
returned: success
type: list
sample: ["example.com"]
hosts_changed:
description: A list of host changed.
returned: success
type: list
sample: ["example.com"]
hosts_change_diff:
description: A list of host diff changes.
returned: on change
type: list
sample: [
{
"example.com": {
"new": {
"hostname": "github.com",
"identityfile": ["/tmp/test_ssh_config/fake_id_rsa"],
"port": "2224"
},
"old": {
"hostname": "github.com",
"identityfile": ["/tmp/test_ssh_config/fake_id_rsa"],
"port": "2224"
}
}
}
]
'''
import os
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_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
class SSHConfig(object):
def __init__(self, module):
self.module = module
if not HAS_PARAMIKO:
module.fail_json(msg=missing_required_lib('PARAMIKO'), exception=PARAMIKO_IMPORT_ERROR)
self.params = module.params
self.user = self.params.get('user')
self.group = self.params.get('group') or self.user
self.host = self.params.get('host')
self.config_file = self.params.get('ssh_config_file')
self.identity_file = self.params['identity_file']
self.check_ssh_config_path()
try:
self.config = ConfigParser(self.config_file)
except FileNotFoundError:
self.module.fail_json(msg="Failed to find %s" % self.config_file)
self.config.load()
def check_ssh_config_path(self):
self.config_file = determine_config_file(self.user, self.config_file)
# See if the identity file exists or not, relative to the config file
if os.path.exists(self.config_file) and self.identity_file is not None:
dirname = os.path.dirname(self.config_file)
self.identity_file = os.path.join(dirname, self.identity_file)
if not os.path.exists(self.identity_file):
self.module.fail_json(msg='IdentityFile %s does not exist' % self.params['identity_file'])
def ensure_state(self):
hosts_result = self.config.search_host(self.host)
state = self.params['state']
args = dict(
hostname=self.params.get('hostname'),
port=self.params.get('port'),
identity_file=self.params.get('identity_file'),
user=self.params.get('remote_user'),
strict_host_key_checking=self.params.get('strict_host_key_checking'),
user_known_hosts_file=self.params.get('user_known_hosts_file'),
proxycommand=self.params.get('proxycommand'),
host_key_algorithms=self.params.get('host_key_algorithms'),
)
# Convert True / False to 'yes' / 'no' for usage in ssh_config
if self.params['forward_agent'] is True:
args['forward_agent'] = 'yes'
if self.params['forward_agent'] is False:
args['forward_agent'] = 'no'
config_changed = False
hosts_changed = []
hosts_change_diff = []
hosts_removed = []
hosts_added = []
hosts_result = [host for host in hosts_result if host['host'] == self.host]
if hosts_result:
for host in hosts_result:
if state == 'absent':
# Delete host from the configuration
config_changed = True
hosts_removed.append(host['host'])
self.config.delete_host(host['host'])
else:
# Update host in the configuration
changed, options = self.change_host(host['options'], **args)
if changed:
config_changed = True
self.config.update_host(host['host'], options)
hosts_changed.append(host['host'])
hosts_change_diff.append({
host['host']: {
'old': host['options'],
'new': options,
}
})
elif state == 'present':
changed, options = self.change_host(dict(), **args)
if changed:
config_changed = True
hosts_added.append(self.host)
self.config.add_host(self.host, options)
if config_changed and not self.module.check_mode:
try:
self.config.write_to_ssh_config()
except PermissionError as perm_exec:
self.module.fail_json(
msg="Failed to write to %s due to permission issue: %s" % (self.config_file, to_native(perm_exec)))
# Make sure we set the permission
perm_mode = '0600'
if self.config_file == '/etc/ssh/ssh_config':
perm_mode = '0644'
self.module.set_mode_if_different(self.config_file, perm_mode, False)
# Make sure the file is owned by the right user and group
self.module.set_owner_if_different(self.config_file, self.user, False)
self.module.set_group_if_different(self.config_file, self.group, False)
self.module.exit_json(changed=config_changed,
hosts_changed=hosts_changed,
hosts_removed=hosts_removed,
hosts_change_diff=hosts_change_diff,
hosts_added=hosts_added)
@staticmethod
def change_host(options, **kwargs):
options = deepcopy(options)
changed = False
for k, v in kwargs.items():
if '_' in k:
k = k.replace('_', '')
if not v:
if options.get(k):
del options[k]
changed = True
elif options.get(k) != v and not (isinstance(options.get(k), list) and v in options.get(k)):
options[k] = v
changed = True
return changed, options
def main():
module = AnsibleModule(
argument_spec=dict(
group=dict(default=None, type='str'),
host=dict(type='str', required=True),
hostname=dict(type='str'),
host_key_algorithms=dict(type='str', no_log=False),
identity_file=dict(type='path'),
port=dict(type='str'),
proxycommand=dict(type='str', default=None),
forward_agent=dict(type='bool'),
remote_user=dict(type='str'),
ssh_config_file=dict(default=None, type='path'),
state=dict(type='str', default='present', choices=['present', 'absent']),
strict_host_key_checking=dict(
default=None,
choices=['yes', 'no', 'ask']
),
user=dict(default=None, type='str'),
user_known_hosts_file=dict(type='str', default=None),
),
supports_check_mode=True,
mutually_exclusive=[
['user', 'ssh_config_file'],
],
)
ssh_config_obj = SSHConfig(module)
ssh_config_obj.ensure_state()
if __name__ == '__main__':
main()