From e500d90c1475ee24443e0e211bd43fcccfff69d4 Mon Sep 17 00:00:00 2001 From: balasubramaniam-k <152608364+balasubramaniam-k@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:56:11 +0530 Subject: [PATCH] SSH resource module implementation (#462) * Implementation of SSH resource module. * Code changes 1. Handling delete of SSH client algorithms (comma separated string) when the algorithms remain same and the order of algorithms in input configuration is different from that of existing. 2. Involving cleanup. * Update plugins/module_utils/network/sonic/config/ssh/ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/module_utils/network/sonic/config/ssh/ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/module_utils/network/sonic/config/ssh/ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/module_utils/network/sonic/config/ssh/ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/module_utils/network/sonic/facts/ssh/ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/modules/sonic_ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/modules/sonic_ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/modules/sonic_ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/modules/sonic_ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/modules/sonic_ssh.py Co-authored-by: Kerry Meyer * Update plugins/modules/sonic_ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update plugins/modules/sonic_ssh.py Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update tests/regression/roles/sonic_ssh/defaults/main.yml Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Update tests/regression/roles/sonic_ssh/defaults/main.yml Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> * Incorporated code review comments. * Incorporated code review comment. --------- Co-authored-by: Balasubramaniam K1 Co-authored-by: stalabi1 <54641848+stalabi1@users.noreply.github.com> Co-authored-by: Kerry Meyer --- .../network/sonic/argspec/facts/facts.py | 3 +- .../network/sonic/argspec/ssh/ssh.py | 65 ++++ .../network/sonic/config/ssh/ssh.py | 354 ++++++++++++++++++ .../module_utils/network/sonic/facts/facts.py | 5 +- .../network/sonic/facts/ssh/ssh.py | 94 +++++ plugins/modules/sonic_facts.py | 1 + plugins/modules/sonic_ssh.py | 292 +++++++++++++++ .../roles/sonic_ssh/defaults/main.yml | 41 ++ .../regression/roles/sonic_ssh/meta/main.yml | 5 + .../roles/sonic_ssh/tasks/cleanup_tests.yaml | 5 + .../regression/roles/sonic_ssh/tasks/main.yml | 17 + .../sonic_ssh/tasks/preparation_tests.yaml | 6 + .../roles/sonic_ssh/tasks/tasks_template.yaml | 22 ++ tests/regression/test.yaml | 1 + .../network/sonic/fixtures/sonic_ssh.yaml | 117 ++++++ .../modules/network/sonic/test_sonic_ssh.py | 81 ++++ 16 files changed, 1107 insertions(+), 2 deletions(-) create mode 100644 plugins/module_utils/network/sonic/argspec/ssh/ssh.py create mode 100644 plugins/module_utils/network/sonic/config/ssh/ssh.py create mode 100644 plugins/module_utils/network/sonic/facts/ssh/ssh.py create mode 100644 plugins/modules/sonic_ssh.py create mode 100644 tests/regression/roles/sonic_ssh/defaults/main.yml create mode 100644 tests/regression/roles/sonic_ssh/meta/main.yml create mode 100644 tests/regression/roles/sonic_ssh/tasks/cleanup_tests.yaml create mode 100644 tests/regression/roles/sonic_ssh/tasks/main.yml create mode 100644 tests/regression/roles/sonic_ssh/tasks/preparation_tests.yaml create mode 100644 tests/regression/roles/sonic_ssh/tasks/tasks_template.yaml create mode 100644 tests/unit/modules/network/sonic/fixtures/sonic_ssh.yaml create mode 100644 tests/unit/modules/network/sonic/test_sonic_ssh.py diff --git a/plugins/module_utils/network/sonic/argspec/facts/facts.py b/plugins/module_utils/network/sonic/argspec/facts/facts.py index 21c32be3f..ecb0c3763 100644 --- a/plugins/module_utils/network/sonic/argspec/facts/facts.py +++ b/plugins/module_utils/network/sonic/argspec/facts/facts.py @@ -80,7 +80,8 @@ def __init__(self, **kwargs): 'login_lockout', 'poe', 'mgmt_servers', - 'ospf_area' + 'ospf_area', + 'ssh', ] argument_spec = { diff --git a/plugins/module_utils/network/sonic/argspec/ssh/ssh.py b/plugins/module_utils/network/sonic/argspec/ssh/ssh.py new file mode 100644 index 000000000..0aee83105 --- /dev/null +++ b/plugins/module_utils/network/sonic/argspec/ssh/ssh.py @@ -0,0 +1,65 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The arg spec for the sonic_ssh module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class SshArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_ssh module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'client': { + 'options': { + 'cipher': { + 'type': 'str' + }, + 'kex': { + 'type': 'str' + }, + 'mac': { + 'type': 'str' + } + }, + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/sonic/config/ssh/ssh.py b/plugins/module_utils/network/sonic/config/ssh/ssh.py new file mode 100644 index 000000000..0e0ae9788 --- /dev/null +++ b/plugins/module_utils/network/sonic/config/ssh/ssh.py @@ -0,0 +1,354 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_ssh class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( + ConfigBase, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import Facts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + get_new_config, + get_formatted_config_diff +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'delete' + + +class Ssh(ConfigBase): + """ + The sonic_ssh class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'ssh', + ] + + ssh_client_config_path = 'data/openconfig-system:system/openconfig-system-ext:ssh-client/config' + ssh_client_algo_config_path = { + 'ciphers': ssh_client_config_path + '/ciphers', + 'kexalgorithms': ssh_client_config_path + '/kexalgorithms', + 'macs': ssh_client_config_path + '/macs' + } + + def __init__(self, module): + super(Ssh, self).__init__(module) + + def get_ssh_facts(self): + """ Get the 'facts' (the current configuration) + + :rtype: A dictionary + :returns: The current configuration as a dictionary + """ + facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources) + ssh_facts = facts['ansible_network_resources'].get('ssh') + if not ssh_facts: + return {} + return ssh_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + commands = list() + + existing_ssh_facts = self.get_ssh_facts() + commands, requests = self.set_config(existing_ssh_facts) + if commands and len(requests) > 0: + if not self._module.check_mode: + try: + edit_config(self._module, to_request(self._module, requests)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + result['changed'] = True + result['commands'] = commands + + changed_ssh_facts = self.get_ssh_facts() + + result['before'] = existing_ssh_facts + if result['changed']: + result['after'] = changed_ssh_facts + + new_config = changed_ssh_facts + old_config = existing_ssh_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_ssh_facts) + result['after(generated)'] = new_config + if self._module._diff: + result['diff'] = get_formatted_config_diff(old_config, new_config, self._module._verbosity) + + result['warnings'] = warnings + return result + + def set_config(self, existing_ssh_facts): + """ Collect the configuration from the args passed to the module, + collect the current configuration (as a dict from facts) + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + want = self._module.params['config'] + have = existing_ssh_facts + resp = self.set_state(want, have) + return to_list(resp) + + def set_state(self, want, have): + """ Select the appropriate function based on the state provided + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + state = self._module.params['state'] + if state == 'overridden': + commands, requests = self._state_replaced_overridden(want, have) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced_overridden(want, have) + return commands, requests + + def _state_replaced_overridden(self, want, have): + """ The command generator when state is replaced or overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + client_want = {} + client_have = {} + if want and want.get('client', None): + client_want['client'] = want['client'] + if have and have.get('client', None): + client_have['client'] = have['client'] + commands, requests = self.handle_ssh_client_replaced_overridden(client_want, client_have) + + return commands, requests + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + requests = [] + + client_want = {} + client_have = {} + if want and want.get('client', None): + client_want['client'] = want['client'] + if have and have.get('client', None): + client_have['client'] = have['client'] + client_commands, client_requests = self.handle_ssh_client_merged(client_want, client_have) + requests.extend(client_requests) + + if client_commands and len(requests) > 0: + commands = update_states(client_commands, 'merged') + + return commands, requests + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + requests = [] + + client_want = {} + client_have = {} + if want and want.get('client', None): + client_want['client'] = want['client'] + if have and have.get('client', None): + client_have['client'] = have['client'] + client_commands, client_requests = self.handle_ssh_client_deleted(client_want, client_have) + requests.extend(client_requests) + + if client_commands and len(requests) > 0: + commands = update_states(client_commands, "deleted") + + return commands, requests + + def handle_ssh_client_replaced_overridden(self, want, have): + """Requests and commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + state = self._module.params['state'] + + if want: + diff = get_diff(have, want) + commands = diff + if commands: + requests = self.delete_specific_ssh_client_params(commands) + commands = update_states(commands, "deleted") + else: + commands = [] + + diff = get_diff(want, have) + if diff: + mod_commands = diff + mod_requests = self.modify_specific_ssh_client_params(mod_commands) + if mod_commands and len(mod_requests) > 0: + mod_commands = update_states(mod_commands, state) + commands.extend(mod_commands) + requests.extend(mod_requests) + + return commands, requests + + def handle_ssh_client_merged(self, want, have): + """Requests and commands necessary to merge the provided into + the current configuration + """ + commands = [] + requests = [] + + diff = get_diff(want, have) + commands = diff + requests.extend(self.modify_specific_ssh_client_params(commands)) + + return commands, requests + + def handle_ssh_client_deleted(self, want, have): + """Requests and commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + requests = [] + + delete_all = False + if not want: + commands = have + delete_all = True + else: + commands = self.get_matched_commands(want, have) + + if commands: + if delete_all: + requests = self.delete_all_ssh_client_params() + else: + requests = self.delete_specific_ssh_client_params(commands) + + return commands, requests + + def modify_specific_ssh_client_params(self, commands): + """Requests to modify specific SSH client algorithm configurations + """ + requests = [] + command = commands.get('client') + + if not command: + return requests + + if 'cipher' in command and command['cipher'] is not None: + payload = {'openconfig-system-ext:ciphers': command['cipher']} + url = self.ssh_client_algo_config_path['ciphers'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + if 'kex' in command and command['kex'] is not None: + payload = {'openconfig-system-ext:kexalgorithms': command['kex']} + url = self.ssh_client_algo_config_path['kexalgorithms'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + if 'mac' in command and command['mac'] is not None: + payload = {'openconfig-system-ext:macs': command['mac']} + url = self.ssh_client_algo_config_path['macs'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + return requests + + def get_matched_commands(self, want, have): + """Matched commands from the input and available configurations + """ + commands = {} + match = {} + if want.get('client') and have.get('client'): + if want['client'].get('cipher') is not None and have['client'].get('cipher') is not None: + cipher = set(want['client'].get('cipher').split(',')) + cfg_cipher = set(have['client'].get('cipher').split(',')) + if cipher == cfg_cipher: + match['cipher'] = ','.join(cipher) + if want['client'].get('kex') is not None and have['client'].get('kex') is not None: + kex = set(want['client'].get('kex').split(',')) + cfg_kex = set(have['client'].get('kex').split(',')) + if kex == cfg_kex: + match['kex'] = ','.join(kex) + if want['client'].get('mac') is not None and have['client'].get('mac') is not None: + mac = set(want['client'].get('mac').split(',')) + cfg_mac = set(have['client'].get('mac').split(',')) + if mac == cfg_mac: + match['mac'] = ','.join(mac) + if match: + commands['client'] = match + + return commands + + def delete_all_ssh_client_params(self): + """Requests to delete SSH client algorithm configurations on the chassis + """ + requests = [] + requests.append({'path': self.ssh_client_config_path, 'method': DELETE}) + + return requests + + def delete_specific_ssh_client_params(self, commands): + """Requests to delete specific SSH client algorithm configurations on the chassis + """ + requests = [] + command = commands.get('client') + + if not command: + return requests + + if 'cipher' in command and command['cipher'] is not None: + requests.append({'path': self.ssh_client_algo_config_path['ciphers'], 'method': DELETE}) + if 'kex' in command and command['kex'] is not None: + requests.append({'path': self.ssh_client_algo_config_path['kexalgorithms'], 'method': DELETE}) + if 'mac' in command and command['mac'] is not None: + requests.append({'path': self.ssh_client_algo_config_path['macs'], 'method': DELETE}) + + return requests diff --git a/plugins/module_utils/network/sonic/facts/facts.py b/plugins/module_utils/network/sonic/facts/facts.py index 2aacd4988..73e12acf8 100644 --- a/plugins/module_utils/network/sonic/facts/facts.py +++ b/plugins/module_utils/network/sonic/facts/facts.py @@ -78,6 +78,8 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.poe.poe import PoeFacts from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.mgmt_servers.mgmt_servers import Mgmt_serversFacts from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.ospf_area.ospf_area import Ospf_areaFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.ssh.ssh import SshFacts + FACT_LEGACY_SUBSETS = {} FACT_RESOURCE_SUBSETS = dict( @@ -140,7 +142,8 @@ login_lockout=Login_lockoutFacts, poe=PoeFacts, mgmt_servers=Mgmt_serversFacts, - ospf_area=Ospf_areaFacts + ospf_area=Ospf_areaFacts, + ssh=SshFacts, ) diff --git a/plugins/module_utils/network/sonic/facts/ssh/ssh.py b/plugins/module_utils/network/sonic/facts/ssh/ssh.py new file mode 100644 index 000000000..f19ea9a24 --- /dev/null +++ b/plugins/module_utils/network/sonic/facts/ssh/ssh.py @@ -0,0 +1,94 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic ssh fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.ssh.ssh import SshArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + +GET = "get" + + +class SshFacts(object): + """ The sonic ssh fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = SshArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for ssh + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + obj = self.get_all_ssh_configs() + + ansible_facts['ansible_network_resources'].pop('ssh', None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {'config': obj}) + facts['ssh'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def get_all_ssh_client_configs(self): + """Get all the SSH client algorithm configurations on the device""" + request = [{"path": "data/openconfig-system:system/openconfig-system-ext:ssh-client/config", "method": GET}] + ssh_client_data = {} + try: + response = edit_config(self._module, to_request(self._module, request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + if 'openconfig-system-ext:config' in response[0][1]: + raw_ssh_client_data = response[0][1]['openconfig-system-ext:config'] + if 'ciphers' in raw_ssh_client_data: + ssh_client_data['cipher'] = raw_ssh_client_data['ciphers'] + if 'kexalgorithms' in raw_ssh_client_data: + ssh_client_data['kex'] = raw_ssh_client_data['kexalgorithms'] + if 'macs' in raw_ssh_client_data: + ssh_client_data['mac'] = raw_ssh_client_data['macs'] + return ssh_client_data + + def get_all_ssh_configs(self): + """Transform OC configuration to Ansible argspec""" + ssh_data = {} + + ssh_client_data = self.get_all_ssh_client_configs() + + if ssh_client_data: + ssh_data['client'] = ssh_client_data + + return ssh_data diff --git a/plugins/modules/sonic_facts.py b/plugins/modules/sonic_facts.py index 7e15383f2..bd5b282c0 100644 --- a/plugins/modules/sonic_facts.py +++ b/plugins/modules/sonic_facts.py @@ -113,6 +113,7 @@ - login_lockout - mgmt_servers - ospf_area + - ssh """ EXAMPLES = """ diff --git a/plugins/modules/sonic_ssh.py b/plugins/modules/sonic_ssh.py new file mode 100644 index 000000000..ae2270305 --- /dev/null +++ b/plugins/modules/sonic_ssh.py @@ -0,0 +1,292 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for sonic_ssh +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_ssh +version_added: '3.0.0' +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). + +short_description: Manage SSH configurations on SONiC +description: + - This module provides SSH configuration management to specify the + algorithms used for SSH connection in devices running SONiC. +author: 'Balasubramaniam Koundappa(@balasubramaniam-k)' +options: + config: + description: + - SSH clients and servers use the following configurations for SSH connections. + type: dict + suboptions: + client: + description: + - SSH client configuration + type: dict + suboptions: + cipher: + description: + - Cipher algorithm used in SSH connection for encryption. + When configured, this value is used by SSH clients + which communicate with the server. + - Specify as a comma separated list. + - Options are aes128-ctr, aes192-ctr, aes256-ctr + - chacha20-poly1305@openssh.com, aes128-gcm@openssh.com + - and aes256-gcm@openssh.com + type: str + kex: + description: + - KEX algorithm used in SSH connection for key exchange. + When configured, this value is used by SSH clients + which communicate with the server. + - Specify as a comma separated list. + - Options are curve25519-sha256, curve25519-sha256@libssh.org + - ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, + - diffie-hellman-group-exchange-sha256, + - diffie-hellman-group16-sha512, + - diffie-hellman-group18-sha512 and + - diffie-hellman-group14-sha256 + type: str + mac: + description: + - MAC algorithm used in SSH connection for generating and + verifying Message Authentication Codes. When configured, + this value is used by SSH clients which communicate with + the server. + - Specify as a comma separated list. + - Options are umac-128-etm@openssh.com, + - hmac-sha2-256-etm@openssh.com, + - hmac-sha2-512-etm@openssh.com, umac-128@openssh.com, + - hmac-sha2-256 and hmac-sha2-512 + type: str + state: + description: + - The state specifies the type of configuration update to be performed on the device. + - If the state is "merged", merge specified attributes with existing configured attributes. + - For "deleted", delete the specified attributes from existing configuration. + - For "replaced", replace on-device SSH configuration with the specified configuration. + - For "overridden", override on-device SSH configurations with the specified configuration. + type: str + choices: + - merged + - deleted + - replaced + - overridden + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client ciphers aes192-ctr,chacha20-poly1305@openssh.com +# ip ssh client macs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com +# ip ssh client kexalgorithms curve25519-sha256,diffie-hellman-group16-sha512 +# sonic# + + - name: Delete specified SSH configurations + dellemc.enterprise_sonic.sonic_ssh: + config: + client: + cipher: 'aes192-ctr,chacha20-poly1305@openssh.com' + mac: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client kexalgorithms curve25519-sha256,diffie-hellman-group16-sha512 +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client ciphers aes192-ctr,chacha20-poly1305@openssh.com +# ip ssh client kexalgorithms curve25519-sha256,diffie-hellman-group16-sha512 +# ip ssh client macs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com +# sonic# + + - name: Delete all SSH configurations + dellemc.enterprise_sonic.sonic_ssh: + config: + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip ssh client" +# (No "ip ssh client" configuration present) +# sonic# + + +# Using merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip ssh client" +# sonic +# (No "ip ssh client" configuration present) + + - name: Modify SSH configurations + dellemc.enterprise_sonic.sonic_ssh: + config: + client: + cipher: 'aes192-ctr,chacha20-poly1305@openssh.com' + mac: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + kex: 'curve25519-sha256,diffie-hellman-group16-sha512' + state: merged + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client ciphers aes192-ctr,chacha20-poly1305@openssh.com +# ip ssh client kexalgorithms curve25519-sha256,diffie-hellman-group16-sha512 +# ip ssh client macs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com +# sonic# + + +# Using replaced +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client ciphers aes192-ctr,chacha20-poly1305@openssh.com +# ip ssh client kexalgorithms curve25519-sha256,diffie-hellman-group16-sha512 +# ip ssh client macs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com +# sonic# + + - name: Modify SSH configurations + dellemc.enterprise_sonic.sonic_ssh: + config: + client: + cipher: 'aes256-ctr' + kex: 'curve25519-sha256,diffie-hellman-group16-sha512' + state: replaced + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client ciphers aes256-ctr +# ip ssh client kexalgorithms curve25519-sha256,diffie-hellman-group16-sha512 +# sonic# + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client ciphers aes192-ctr,chacha20-poly1305@openssh.com +# ip ssh client kexalgorithms curve25519-sha256,diffie-hellman-group16-sha512 +# ip ssh client macs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com +# sonic# + + - name: Modify SSH configurations + dellemc.enterprise_sonic.sonic_ssh: + config: + client: + cipher: 'aes256-ctr' + mac: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + state: overridden + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip ssh client" +# ip ssh client ciphers aes256-ctr +# ip ssh client macs umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com +# sonic# + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + as the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + as the parameters above. +after(generated): + description: The generated configuration from module invocation. + returned: when C(check_mode) + type: dict + sample: > + The configuration returned will always be in the same format + as the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['command 1', 'command 2', 'command 3'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.ssh.ssh import SshArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.ssh.ssh import Ssh + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=SshArgs.argument_spec, + supports_check_mode=True) + + result = Ssh(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/regression/roles/sonic_ssh/defaults/main.yml b/tests/regression/roles/sonic_ssh/defaults/main.yml new file mode 100644 index 000000000..446a93fd0 --- /dev/null +++ b/tests/regression/roles/sonic_ssh/defaults/main.yml @@ -0,0 +1,41 @@ +--- +ansible_connection: httpapi +module_name: ssh + +tests: + - name: test_case_01 + description: Add SSH client algorithm configuration + state: merged + input: + client: + mac: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + kex: 'curve25519-sha256,diffie-hellman-group16-sha512' + + - name: test_case_02 + description: Delete SSH client algorithm configuration + state: deleted + input: + client: + cipher: 'aes192-ctr,chacha20-poly1305@openssh.com' + kex: 'diffie-hellman-group16-sha512,curve25519-sha256' + + - name: test_case_03 + description: Replace SSH client algorithm configuration + state: replaced + input: + client: + cipher: 'aes192-ctr,chacha20-poly1305@openssh.com' + kex: 'curve25519-sha256' + + - name: test_case_04 + description: Override SSH client algorithm configuration + state: overridden + input: + client: + cipher: 'aes192-ctr' + kex: 'diffie-hellman-group16-sha512' + + - name: test_case_05 + description: Delete all SSH client algorithm configurations + state: deleted + input: {} diff --git a/tests/regression/roles/sonic_ssh/meta/main.yml b/tests/regression/roles/sonic_ssh/meta/main.yml new file mode 100644 index 000000000..d0ceaf6f5 --- /dev/null +++ b/tests/regression/roles/sonic_ssh/meta/main.yml @@ -0,0 +1,5 @@ +--- +collections: + - dellemc.enterprise_sonic +dependencies: + - { role: common } diff --git a/tests/regression/roles/sonic_ssh/tasks/cleanup_tests.yaml b/tests/regression/roles/sonic_ssh/tasks/cleanup_tests.yaml new file mode 100644 index 000000000..1e5c13045 --- /dev/null +++ b/tests/regression/roles/sonic_ssh/tasks/cleanup_tests.yaml @@ -0,0 +1,5 @@ +- name: Delete SSH client algorithm configuration + dellemc.enterprise_sonic.sonic_ssh: + config: + state: deleted + ignore_errors: yes diff --git a/tests/regression/roles/sonic_ssh/tasks/main.yml b/tests/regression/roles/sonic_ssh/tasks/main.yml new file mode 100644 index 000000000..352f2c716 --- /dev/null +++ b/tests/regression/roles/sonic_ssh/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- ansible.builtin.debug: + msg: "sonic_ssh Test started ..." + +- name: "Preparations for {{ module_name }}" + ansible.builtin.include_tasks: preparation_tests.yaml + +- name: "Test {{ module_name }} started" + ansible.builtin.include_tasks: tasks_template.yaml + loop: "{{ tests }}" + +- name: "Cleanup of {{ module_name }}" + ansible.builtin.include_tasks: cleanup_tests.yaml + +- name: Display all variables/facts known for a host + ansible.builtin.debug: + var: hostvars[inventory_hostname].ansible_facts.test_reports diff --git a/tests/regression/roles/sonic_ssh/tasks/preparation_tests.yaml b/tests/regression/roles/sonic_ssh/tasks/preparation_tests.yaml new file mode 100644 index 000000000..d88e0aa78 --- /dev/null +++ b/tests/regression/roles/sonic_ssh/tasks/preparation_tests.yaml @@ -0,0 +1,6 @@ +--- +- name: Delete old SSH client algorithm configurations + dellemc.enterprise_sonic.sonic_ssh: + config: + state: deleted + ignore_errors: yes diff --git a/tests/regression/roles/sonic_ssh/tasks/tasks_template.yaml b/tests/regression/roles/sonic_ssh/tasks/tasks_template.yaml new file mode 100644 index 000000000..55336d7ef --- /dev/null +++ b/tests/regression/roles/sonic_ssh/tasks/tasks_template.yaml @@ -0,0 +1,22 @@ +--- +- name: "{{ item.name }} , {{ item.description }}" + dellemc.enterprise_sonic.sonic_ssh: + config: "{{ item.input }}" + state: "{{ item.state }}" + register: action_task_output + ignore_errors: yes + +- ansible.builtin.import_role: + name: common + tasks_from: action.facts.report.yaml + +- name: "{{ item.name }} , {{ item.description }} Idempotent" + dellemc.enterprise_sonic.sonic_ssh: + config: "{{ item.input }}" + state: "{{ item.state }}" + register: idempotent_task_output + ignore_errors: yes + +- ansible.builtin.import_role: + name: common + tasks_from: idempotent.facts.report.yaml diff --git a/tests/regression/test.yaml b/tests/regression/test.yaml index 87c657c77..5ea3065b3 100644 --- a/tests/regression/test.yaml +++ b/tests/regression/test.yaml @@ -71,4 +71,5 @@ - sonic_ospf_area - sonic_poe - sonic_mgmt_servers + - sonic_ssh - test_reports diff --git a/tests/unit/modules/network/sonic/fixtures/sonic_ssh.yaml b/tests/unit/modules/network/sonic/fixtures/sonic_ssh.yaml new file mode 100644 index 000000000..f6b59ac12 --- /dev/null +++ b/tests/unit/modules/network/sonic/fixtures/sonic_ssh.yaml @@ -0,0 +1,117 @@ +--- +merged_01: + module_args: + config: + client: + cipher: 'aes192-ctr,chacha20-poly1305@openssh.com' + kex: 'curve25519-sha256,diffie-hellman-group16-sha512' + mac: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + state: merged + facts_get_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config" + response: + code: 200 + value: + openconfig-system-ext:config: + ciphers: 'aes256-ctr' + kexalgorithms: 'curve25519-sha256,curve25519-sha256@libssh.org' + macs: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + config_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/ciphers" + method: "patch" + data: + openconfig-system-ext:ciphers: 'aes192-ctr,chacha20-poly1305@openssh.com' + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/kexalgorithms" + method: "patch" + data: + openconfig-system-ext:kexalgorithms: 'curve25519-sha256,diffie-hellman-group16-sha512' +deleted_01: + module_args: + config: + client: + cipher: 'aes192-ctr,chacha20-poly1305@openssh.com' + kex: 'curve25519-sha256,diffie-hellman-group16-sha512' + mac: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + state: deleted + facts_get_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config" + response: + code: 200 + value: + openconfig-system-ext:config: + ciphers: 'aes192-ctr,chacha20-poly1305@openssh.com' + kexalgorithms: 'curve25519-sha256,diffie-hellman-group16-sha512' + macs: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + config_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/ciphers" + method: "delete" + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/kexalgorithms" + method: "delete" + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/macs" + method: "delete" +deleted_02: + module_args: + config: + state: deleted + facts_get_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config" + response: + code: 200 + value: + openconfig-system-ext:config: + ciphers: 'aes192-ctr,chacha20-poly1305@openssh.com' + kexalgorithms: 'curve25519-sha256,diffie-hellman-group16-sha512' + macs: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + config_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config" + method: "delete" +replaced_01: + module_args: + config: + client: + cipher: 'aes256-ctr' + kex: 'curve25519-sha256,diffie-hellman-group16-sha512' + state: replaced + facts_get_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config" + response: + code: 200 + value: + openconfig-system-ext:config: + ciphers: 'aes192-ctr,chacha20-poly1305@openssh.com' + kexalgorithms: 'curve25519-sha256,diffie-hellman-group16-sha512' + macs: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + config_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/ciphers" + method: "delete" + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/macs" + method: "delete" + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/ciphers" + method: "patch" + data: + openconfig-system-ext:ciphers: 'aes256-ctr' +overridden_01: + module_args: + config: + client: + cipher: 'aes256-ctr' + mac: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + state: overridden + facts_get_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config" + response: + code: 200 + value: + openconfig-system-ext:config: + ciphers: 'aes192-ctr,chacha20-poly1305@openssh.com' + kexalgorithms: 'curve25519-sha256,diffie-hellman-group16-sha512' + macs: 'umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com' + config_requests: + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/ciphers" + method: "delete" + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/kexalgorithms" + method: "delete" + - path: "data/openconfig-system:system/openconfig-system-ext:ssh-client/config/ciphers" + method: "patch" + data: + openconfig-system-ext:ciphers: 'aes256-ctr' diff --git a/tests/unit/modules/network/sonic/test_sonic_ssh.py b/tests/unit/modules/network/sonic/test_sonic_ssh.py new file mode 100644 index 000000000..fb1756396 --- /dev/null +++ b/tests/unit/modules/network/sonic/test_sonic_ssh.py @@ -0,0 +1,81 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.dellemc.enterprise_sonic.tests.unit.compat.mock import ( + patch, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.modules import ( + sonic_ssh, +) +from ansible_collections.dellemc.enterprise_sonic.tests.unit.modules.utils import ( + set_module_args, +) +from .sonic_module import TestSonicModule + + +class TestSonicSshModule(TestSonicModule): + module = sonic_ssh + + @classmethod + def setUpClass(cls): + cls.mock_facts_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.ssh.ssh.edit_config" + ) + cls.mock_config_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.ssh.ssh.edit_config" + ) + cls.fixture_data = cls.load_fixtures('sonic_ssh.yaml') + + def setUp(self): + super(TestSonicSshModule, self).setUp() + self.facts_edit_config = self.mock_facts_edit_config.start() + self.config_edit_config = self.mock_config_edit_config.start() + + self.facts_edit_config.side_effect = self.facts_side_effect + self.config_edit_config.side_effect = self.config_side_effect + + def tearDown(self): + super(TestSonicSshModule, self).tearDown() + self.mock_facts_edit_config.stop() + self.mock_config_edit_config.stop() + + def test_sonic_ssh_merged_01(self): + set_module_args(self.fixture_data['merged_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['merged_01']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['merged_01']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_ssh_deleted_01(self): + set_module_args(self.fixture_data['deleted_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_01']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['deleted_01']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_ssh_deleted_02(self): + set_module_args(self.fixture_data['deleted_02']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_02']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['deleted_02']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_ssh_replaced_01(self): + set_module_args(self.fixture_data['replaced_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['replaced_01']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['replaced_01']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_ssh_overridden_01(self): + set_module_args(self.fixture_data['overridden_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['overridden_01']['facts_get_requests']) + self.initialize_config_requests(self.fixture_data['overridden_01']['config_requests']) + + result = self.execute_module(changed=True) + self.validate_config_requests()