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/system/ssh_config.py
Anton Nikolaev 1a4af9bfc3
Reduce stormssh searches based on host (#2568)
* 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>
2021-06-05 14:53:02 +02:00

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()