mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
1a4af9bfc3
* Reduce stormssh searches based on host Due to the stormssh searches in the whole config values, we need to reduce the search results based on the full matching of the hosts * Removed whitespaces in the blank line * Added changelog fragment and tests for the fix. * Added newline at the end of the changelog fragment * Added newline at the end of the tests * Fixed bug with name in tests * Changed assertion for the existing host * Update changelogs/fragments/2568-ssh_config-reduce-stormssh-searches-based-on-host.yml Co-authored-by: Felix Fontein <felix@fontein.de> * Adjusted tests * New line at the end of the tests Co-authored-by: Anton Nikolaev <anikolaev@apple.com> Co-authored-by: Felix Fontein <felix@fontein.de>
316 lines
10 KiB
Python
316 lines
10 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 COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
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)
|
|
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
|
|
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
|
|
requirements:
|
|
- StormSSH
|
|
notes:
|
|
- Supports check mode.
|
|
'''
|
|
|
|
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
|
|
import traceback
|
|
|
|
from copy import deepcopy
|
|
|
|
STORM_IMP_ERR = None
|
|
try:
|
|
from storm.parsers.ssh_config_parser import ConfigParser
|
|
HAS_STORM = True
|
|
except ImportError:
|
|
HAS_STORM = False
|
|
STORM_IMP_ERR = traceback.format_exc()
|
|
|
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
from ansible.module_utils._text import to_native
|
|
|
|
|
|
class SSHConfig():
|
|
def __init__(self, module):
|
|
self.module = module
|
|
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):
|
|
if self.user:
|
|
self.config_file = os.path.join(os.path.expanduser('~%s' % self.user), '.ssh', 'config')
|
|
elif self.config_file is None:
|
|
self.config_file = '/etc/ssh/ssh_config'
|
|
|
|
# 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'),
|
|
)
|
|
|
|
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'),
|
|
identity_file=dict(type='path'),
|
|
port=dict(type='str'),
|
|
proxycommand=dict(type='str', default=None),
|
|
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'],
|
|
],
|
|
)
|
|
|
|
if not HAS_STORM:
|
|
module.fail_json(changed=False, msg=missing_required_lib("stormssh"),
|
|
exception=STORM_IMP_ERR)
|
|
|
|
ssh_config_obj = SSHConfig(module)
|
|
ssh_config_obj.ensure_state()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|