diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/dellemc/enterprise_sonic/plugins | |
parent | Initial commit. (diff) | |
download | ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip |
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/dellemc/enterprise_sonic/plugins')
162 files changed, 27038 insertions, 0 deletions
diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/action/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/action/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/action/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/action/sonic.py b/ansible_collections/dellemc/enterprise_sonic/plugins/action/sonic.py new file mode 100644 index 000000000..5f7ac3a82 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/action/sonic.py @@ -0,0 +1,51 @@ +# +# (c) 2020 Red Hat Inc. +# +# (c) 2020 Dell Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +from ansible_collections.ansible.netcommon.plugins.action.network import ActionModule as ActionNetworkModule + +from ansible.utils.display import Display + +display = Display() + +DOCUMENTATION = """ +short_description: Action plugin module for sonic CLI modules +version_added: 1.0.0 +""" + + +class ActionModule(ActionNetworkModule): + + def run(self, task_vars=None): + + module_name = self._task.action.split('.')[-1] + self._config_module = True if module_name == 'sonic_config' else False + + if self._play_context.connection in ('network_cli', 'httpapi'): + provider = self._task.args.get('provider', {}) + if any(provider.values()): + display.warning('provider is unnecessary when using network_cli and will be ignored') + del self._task.args['provider'] + + result = super(ActionModule, self).run(task_vars=task_vars) + return result diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/sonic.py b/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/sonic.py new file mode 100644 index 000000000..37f1d872a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/sonic.py @@ -0,0 +1,118 @@ +# +# (c) 2020 Red Hat Inc. +# +# (c) 2020 Dell Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ +--- +name: sonic +short_description: Use sonic cliconf to run command on Dell OS10 platform +description: + - This sonic plugin provides low level abstraction apis for + sending and receiving CLI commands from Dell OS10 network devices. +""" + +import json + +from itertools import chain + +from ansible.errors import AnsibleConnectionFailure +from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common._collections_compat import Mapping +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list +from ansible.plugins.cliconf import CliconfBase, enable_mode + + +class Cliconf(CliconfBase): + + def get_device_info(self): + device_info = {} + device_info['network_os'] = 'sonic' + return device_info + + @enable_mode + def edit_config(self, command): + response = [] + self.send_command("configure terminal") + for cmd in to_list(command): + if isinstance(cmd, dict): + resp = self.get(command=cmd["command"], prompt=cmd["prompt"], answer=cmd["answer"]) + response.append(resp) + else: + response.append(self.send_command(to_bytes(cmd))) + self.send_command("end") + return response + + @enable_mode + def get_config(self, source="running", flags=None, format=None): + if source not in ("running", "startup"): + raise ValueError( + "fetching configuration from %s is not supported" % source + ) + if not flags: + flags = [] + if source == "running": + cmd = "show running-config " + else: + cmd = "show startup-config " + + cmd += " ".join(to_list(flags)) + cmd = cmd.strip() + return self.send_command(cmd) + + def get(self, command, prompt=None, answer=None, sendonly=False, newline=True, check_all=False): + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, newline=newline, check_all=check_all) + + def get_capabilities(self): + result = super(Cliconf, self).get_capabilities() + return json.dumps(result) + + def run_commands(self, commands=None, check_rc=True): + if commands is None: + raise ValueError("'commands' value is required") + + responses = list() + for cmd in to_list(commands): + if not isinstance(cmd, Mapping): + cmd = {'command': cmd} + + output = cmd.pop('output', None) + if output: + raise ValueError("'output' value %s is not supported for run_commands" % output) + + try: + out = self.send_command(**cmd) + except AnsibleConnectionFailure as e: + if check_rc: + raise + out = getattr(e, 'err', to_text(e)) + + responses.append(out) + + return responses + + def set_cli_prompt_context(self): + """ + Make sure we are in the operational cli mode + :return: None + """ + if self._connection.connected: + self._update_cli_prompt_context(config_context=')#') diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/httpapi/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/httpapi/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/httpapi/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/httpapi/sonic.py b/ansible_collections/dellemc/enterprise_sonic/plugins/httpapi/sonic.py new file mode 100644 index 000000000..4745e2e9f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/httpapi/sonic.py @@ -0,0 +1,113 @@ +# (c) 2019 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = """ +--- +name: sonic +short_description: HttpApi Plugin for devices supporting Restconf SONIC API +description: + - This HttpApi plugin provides methods to connect to Restconf SONIC API endpoints. +version_added: 1.0.0 +options: + root_path: + type: str + description: + - Specifies the location of the Restconf root. + default: '/restconf' + vars: + - name: ansible_httpapi_restconf_root +""" + +import json + +from ansible.module_utils._text import to_text +from ansible.module_utils.connection import ConnectionError +from ansible.module_utils.six.moves.urllib.error import HTTPError +from ansible.plugins.httpapi import HttpApiBase +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list + +CONTENT_TYPE = 'application/yang-data+json' + + +class HttpApi(HttpApiBase): + def send_request(self, data, **message_kwargs): + if data: + data = json.dumps(data) + + path = '/'.join([self.get_option('root_path').rstrip('/'), message_kwargs.get('path', '').lstrip('/')]) + + headers = { + 'Content-Type': message_kwargs.get('content_type') or CONTENT_TYPE, + 'Accept': message_kwargs.get('accept') or CONTENT_TYPE, + } + response, response_data = self.connection.send(path, data, headers=headers, method=message_kwargs.get('method')) + + return handle_response(response, response_data, message_kwargs) + + def get(self, command): + return self.send_request(path=command, data=None, method='get') + + def edit_config(self, requests): + """Send a list of http requests to remote device and return results + """ + if requests is None: + raise ValueError("'requests' value is required") + + responses = list() + for req in to_list(requests): + try: + response = self.send_request(**req) + except ConnectionError as exc: + raise ConnectionError(to_text(exc, errors='surrogate_then_replace')) + responses.append(response) + return responses + + def get_capabilities(self): + result = {} + result['rpc'] = [] + result['network_api'] = 'sonic_rest' + + return json.dumps(result) + + +def handle_response(response, response_data, request_data): + response_data = response_data.read() + try: + if not response_data: + response_data = "" + else: + response_data = json.loads(response_data.decode('utf-8')) + except ValueError: + pass + + if isinstance(response, HTTPError): + if response_data: + if 'errors' in response_data: + errors = response_data['errors']['error'] + error_text = '\n'.join((error['error-message'] for error in errors)) + else: + error_text = response_data + error_text.update({u'code': response.code}) + error_text.update({u'request_data': request_data}) + raise ConnectionError(error_text, code=response.code) + raise ConnectionError(to_text(response), code=response.code) + return response.getcode(), response_data diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/aaa/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/aaa/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/aaa/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/aaa/aaa.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/aaa/aaa.py new file mode 100644 index 000000000..86040892a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/aaa/aaa.py @@ -0,0 +1,66 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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_aaa module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class AaaArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_aaa module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'authentication': { + 'options': { + 'data': { + 'options': { + 'fail_through': {'type': 'bool'}, + 'group': { + 'choices': ['ldap', 'radius', 'tacacs+'], + 'type': 'str' + }, + 'local': {'type': 'bool'} + }, + 'type': 'dict' + } + }, + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp/bgp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp/bgp.py new file mode 100644 index 000000000..fb7618133 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp/bgp.py @@ -0,0 +1,97 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_bgp module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class BgpArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bgp module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'bestpath': { + 'options': { + 'as_path': { + 'options': { + 'confed': {'type': 'bool'}, + 'ignore': {'type': 'bool'}, + 'multipath_relax': {'type': 'bool'}, + 'multipath_relax_as_set': {'type': 'bool'} + }, + 'type': 'dict' + }, + 'compare_routerid': {'type': 'bool'}, + 'med': { + 'options': { + 'confed': {'type': 'bool'}, + 'missing_as_worst': {'type': 'bool'}, + 'always_compare_med': {'type': 'bool'} + }, + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'bgp_as': {'required': True, 'type': 'str'}, + 'log_neighbor_changes': {'type': 'bool'}, + 'router_id': {'type': 'str'}, + "max_med": { + "options": { + "on_startup": { + "options": { + "timer": {"type": "int"}, + "med_val": {"type": "int"} + }, + "type": "dict" + } + }, + "type": "dict" + }, + 'timers': { + 'options': { + 'holdtime': {'type': 'int'}, + 'keepalive_interval': {'type': 'int'} + }, + 'type': 'dict' + }, + 'vrf_name': {'default': 'default', 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_af/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_af/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_af/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_af/bgp_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_af/bgp_af.py new file mode 100644 index 000000000..ac22210ee --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_af/bgp_af.py @@ -0,0 +1,117 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_bgp_af module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Bgp_afArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bgp_af module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'address_family': { + 'options': { + 'afis': { + 'elements': 'dict', + 'options': { + 'advertise_pip': {'type': 'bool'}, + 'advertise_pip_ip': {'type': 'str'}, + 'advertise_pip_peer_ip': {'type': 'str'}, + 'advertise_svi_ip': {'type': 'bool'}, + 'route_advertise_list': { + 'elements': 'dict', + 'options': { + 'advertise_afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + }, + 'route_map': { + 'type': 'str' + } + }, + 'type': 'list' + }, + 'advertise_all_vni': {'type': 'bool'}, + 'advertise_default_gw': {'type': 'bool'}, + 'afi': { + 'choices': ['ipv4', 'ipv6', 'l2vpn'], + 'required': True, + 'type': 'str' + }, + 'max_path': { + 'options': { + 'ebgp': {'type': 'int'}, + 'ibgp': {'type': 'int'} + }, + 'type': 'dict' + }, + 'network': {'type': 'list', 'elements': 'str'}, + 'dampening': {'type': 'bool'}, + 'redistribute': { + 'elements': 'dict', + 'options': { + 'metric': {'type': 'str'}, + 'protocol': { + 'choices': ['ospf', 'static', 'connected'], + 'required': True, + 'type': 'str' + }, + 'route_map': {'type': 'str'} + }, + 'type': 'list' + }, + 'safi': { + 'choices': ['unicast', 'evpn'], + 'default': 'unicast', + 'type': 'str' + } + }, + 'required_together': [['afi', 'safi']], + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'bgp_as': {'required': True, 'type': 'str'}, + 'vrf_name': {'default': 'default', 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_as_paths/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_as_paths/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_as_paths/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_as_paths/bgp_as_paths.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_as_paths/bgp_as_paths.py new file mode 100644 index 000000000..dec9b930e --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_as_paths/bgp_as_paths.py @@ -0,0 +1,48 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_as_paths module +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Bgp_as_pathsArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bgp_as_paths module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = {'config': {'elements': 'dict', + 'options': {'permit': {'required': False, 'type': 'bool'}, + 'members': {'elements': 'str', + 'required': False, + 'type': 'list'}, + 'name': {'required': True, 'type': 'str'}}, + 'type': 'list'}, + 'state': {'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str'}} # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_communities/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_communities/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_communities/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_communities/bgp_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_communities/bgp_communities.py new file mode 100644 index 000000000..867e55204 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_communities/bgp_communities.py @@ -0,0 +1,59 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_communities module +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Bgp_communitiesArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bgp_communities module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = {'config': {'elements': 'dict', + 'options': {'aann': {'type': 'str'}, + 'local_as': {'type': 'bool'}, + 'match': {'choices': ['ALL', 'ANY'], + 'default': 'ANY', + 'type': 'str'}, + 'members': {'options': {'regex': {'elements': 'str', + 'type': 'list'}}, + 'type': 'dict'}, + 'name': {'required': True, 'type': 'str'}, + 'no_advertise': {'type': 'bool'}, + 'no_export': {'type': 'bool'}, + 'no_peer': {'type': 'bool'}, + 'permit': {'type': 'bool'}, + 'type': {'choices': ['standard', 'expanded'], + 'default': 'standard', + 'type': 'str'}}, + 'type': 'list'}, + 'state': {'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str'}} # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_ext_communities/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_ext_communities/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_ext_communities/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_ext_communities/bgp_ext_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_ext_communities/bgp_ext_communities.py new file mode 100644 index 000000000..aec0f364a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_ext_communities/bgp_ext_communities.py @@ -0,0 +1,75 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_ext_communities module +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Bgp_ext_communitiesArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bgp_ext_communities module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'match': { + 'choices': ['all', 'any'], + 'default': 'any', + 'type': 'str' + }, + 'members': { + 'mutually_exclusive': [ + ['regex', 'route_origin'], + ['regex', 'route_target'] + ], + 'options': { + 'regex': {'elements': 'str', 'type': 'list'}, + 'route_origin': {'elements': 'str', 'type': 'list'}, + 'route_target': {'elements': 'str', 'type': 'list'} + }, + 'type': 'dict' + }, + 'name': {'required': True, 'type': 'str'}, + 'permit': {'type': 'bool'}, + 'type': { + 'choices': ['standard', 'expanded'], + 'default': 'standard', + 'type': 'str' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors/bgp_neighbors.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors/bgp_neighbors.py new file mode 100644 index 000000000..02e695fb4 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors/bgp_neighbors.py @@ -0,0 +1,249 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_bgp_neighbors module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Bgp_neighborsArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bgp_neighbors module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'bgp_as': {'required': True, 'type': 'str'}, + 'neighbors': { + 'elements': 'dict', + 'options': { + 'neighbor': {'required': True, 'type': 'str'}, + 'remote_as': { + 'mutually_exclusive': [['peer_type', 'peer_as']], + 'options': { + 'peer_type': {'type': 'str', 'choices': ['internal', 'external']}, + 'peer_as': {'type': 'int'}, + }, + 'type': 'dict' + }, + 'peer_group': {'type': 'str'}, + 'bfd': { + 'options': { + 'enabled': {'type': 'bool'}, + 'check_failure': {'type': 'bool'}, + 'profile': {'type': 'str'} + }, + 'type': 'dict' + }, + 'advertisement_interval': {'type': 'int'}, + 'timers': { + 'options': { + 'holdtime': {'type': 'int'}, + 'keepalive': {'type': 'int'}, + 'connect_retry': {'type': 'int'} + }, + 'type': 'dict' + }, + 'capability': { + 'options': { + 'dynamic': {'type': 'bool'}, + 'extended_nexthop': {'type': 'bool'}, + }, + 'type': 'dict' + }, + 'auth_pwd': { + 'options': { + 'pwd': {'required': True, 'type': 'str'}, + 'encrypted': {'default': 'False', 'type': 'bool'}, + }, + 'type': 'dict' + }, + 'nbr_description': {'type': 'str'}, + 'disable_connected_check': {'type': 'bool'}, + 'dont_negotiate_capability': {'type': 'bool'}, + 'ebgp_multihop': { + 'options': { + 'enabled': {'default': 'False', 'type': 'bool'}, + 'multihop_ttl': {'type': 'int'} + }, + 'type': 'dict' + }, + 'enforce_first_as': {'type': 'bool'}, + 'enforce_multihop': {'type': 'bool'}, + 'local_address': {'type': 'str'}, + 'local_as': { + 'options': { + 'as': {'required': True, 'type': 'int'}, + 'no_prepend': {'type': 'bool'}, + 'replace_as': {'type': 'bool'}, + }, + 'type': 'dict' + }, + 'override_capability': {'type': 'bool'}, + 'passive': {'default': 'False', 'type': 'bool'}, + 'port': {'type': 'int'}, + 'shutdown_msg': {'type': 'str'}, + 'solo': {'type': 'bool'}, + 'strict_capability_match': {'type': 'bool'}, + 'ttl_security': {'type': 'int'}, + 'v6only': {'type': 'bool'} + }, + 'type': 'list' + }, + 'peer_group': { + 'elements': 'dict', + 'options': { + 'name': {'required': True, 'type': 'str'}, + 'remote_as': { + 'mutually_exclusive': [['peer_type', 'peer_as']], + 'options': { + 'peer_type': {'type': 'str', 'choices': ['internal', 'external']}, + 'peer_as': {'type': 'int'}, + }, + 'type': 'dict' + }, + 'address_family': { + 'options': { + 'afis': { + 'elements': 'dict', + 'options': { + 'activate': {'type': 'bool'}, + 'afi': { + 'choices': ['ipv4', 'ipv6', 'l2vpn'], + 'type': 'str' + }, + 'allowas_in': { + 'mutually_exclusive': [['origin', 'value']], + 'options': { + 'origin': {'type': 'bool'}, + 'value': {'type': 'int'} + }, + 'type': 'dict' + }, + 'ip_afi': { + 'options': { + 'default_policy_name': {'type': 'str'}, + 'send_default_route': {'default': False, 'type': 'bool'} + }, + 'type': 'dict' + }, + 'prefix_limit': { + 'options': { + 'max_prefixes': {'type': 'int'}, + 'prevent_teardown': {'default': False, 'type': 'bool'}, + 'warning_threshold': {'type': 'int'}, + 'restart_timer': {'type': 'int'} + }, + 'type': 'dict' + }, + 'prefix_list_in': {'type': 'str'}, + 'prefix_list_out': {'type': 'str'}, + 'safi': { + 'choices': ['unicast', 'evpn'], + 'type': 'str' + }, + }, + 'required_together': [['afi', 'safi']], + 'type': 'list' + }, + }, + 'type': 'dict' + }, + 'bfd': { + 'options': { + 'enabled': {'type': 'bool'}, + 'check_failure': {'type': 'bool'}, + 'profile': {'type': 'str'} + }, + 'type': 'dict' + }, + 'advertisement_interval': {'type': 'int'}, + 'timers': { + 'options': { + 'holdtime': {'type': 'int'}, + 'keepalive': {'type': 'int'}, + 'connect_retry': {'type': 'int'} + }, + 'type': 'dict' + }, + 'capability': { + 'options': { + 'dynamic': {'type': 'bool'}, + 'extended_nexthop': {'type': 'bool'}, + }, + 'type': 'dict' + }, + 'auth_pwd': { + 'options': { + 'pwd': {'required': True, 'type': 'str'}, + 'encrypted': {'default': 'False', 'type': 'bool'}, + }, + 'type': 'dict' + }, + 'pg_description': {'type': 'str'}, + 'disable_connected_check': {'type': 'bool'}, + 'dont_negotiate_capability': {'type': 'bool'}, + 'ebgp_multihop': { + 'options': { + 'enabled': {'default': 'False', 'type': 'bool'}, + 'multihop_ttl': {'type': 'int'} + }, + 'type': 'dict' + }, + 'enforce_first_as': {'type': 'bool'}, + 'enforce_multihop': {'type': 'bool'}, + 'local_address': {'type': 'str'}, + 'local_as': { + 'options': { + 'as': {'required': True, 'type': 'int'}, + 'no_prepend': {'type': 'bool'}, + 'replace_as': {'type': 'bool'}, + }, + 'type': 'dict' + }, + 'override_capability': {'type': 'bool'}, + 'passive': {'default': 'False', 'type': 'bool'}, + 'shutdown_msg': {'type': 'str'}, + 'solo': {'type': 'bool'}, + 'strict_capability_match': {'type': 'bool'}, + 'ttl_security': {'type': 'int'} + }, + 'type': 'list' + }, + 'vrf_name': {'default': 'default', 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors_af/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors_af/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors_af/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors_af/bgp_neighbors_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors_af/bgp_neighbors_af.py new file mode 100644 index 000000000..6cafc9227 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bgp_neighbors_af/bgp_neighbors_af.py @@ -0,0 +1,114 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_bgp_neighbors_af module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Bgp_neighbors_afArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bgp_neighbors_af module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'bgp_as': {'required': True, 'type': 'str'}, + 'neighbors': { + 'elements': 'dict', + 'options': { + 'address_family': { + 'elements': 'dict', + 'options': { + 'activate': {'type': 'bool'}, + 'afi': { + 'choices': ['ipv4', 'ipv6', 'l2vpn'], + 'required': True, + 'type': 'str' + }, + 'allowas_in': { + 'mutually_exclusive': [['origin', 'value']], + 'options': { + 'origin': {'type': 'bool'}, + 'value': {'type': 'int'} + }, + 'type': 'dict' + }, + 'ip_afi': { + 'options': { + 'default_policy_name': {'type': 'str'}, + 'send_default_route': {'default': False, 'type': 'bool'} + }, + 'type': 'dict' + }, + 'prefix_limit': { + 'options': { + 'max_prefixes': {'type': 'int'}, + 'prevent_teardown': {'default': False, 'type': 'bool'}, + 'warning_threshold': {'type': 'int'}, + 'restart_timer': {'type': 'int'} + }, + 'type': 'dict' + }, + 'prefix_list_in': {'type': 'str'}, + 'prefix_list_out': {'type': 'str'}, + 'route_map': { + 'elements': 'dict', + 'options': { + 'direction': {'type': 'str'}, + 'name': {'type': 'str'} + }, + 'type': 'list' + }, + 'route_reflector_client': {'type': 'bool'}, + 'route_server_client': {'type': 'bool'}, + 'safi': { + 'choices': ['unicast', 'evpn'], + 'default': 'unicast', + 'type': 'str' + } + }, + 'required_together': [['afi', 'safi']], + 'type': 'list' + }, + 'neighbor': {'required': True, 'type': 'str'} + }, + 'type': 'list' + }, + 'vrf_name': {'default': 'default', 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/facts/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/facts/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/facts/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/facts/facts.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/facts/facts.py new file mode 100644 index 000000000..3a4d02989 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/facts/facts.py @@ -0,0 +1,53 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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 arg spec for the sonic facts module. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class FactsArgs(object): # pylint: disable=R0903 + + """ The arg spec for the sonic facts module + """ + + def __init__(self, **kwargs): + pass + + choices = [ + 'all', + 'vlans', + 'interfaces', + 'l2_interfaces', + 'l3_interfaces', + 'lag_interfaces', + 'bgp', + 'bgp_af', + 'bgp_neighbors', + 'bgp_neighbors_af', + 'bgp_as_paths', + 'bgp_communities', + 'bgp_ext_communities', + 'mclag', + 'prefix_lists', + 'vrfs', + 'vxlans', + 'users', + 'system', + 'port_breakout', + 'aaa', + 'tacacs_server', + 'radius_server', + 'static_routes', + 'ntp' + ] + + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list', elements='str'), + 'gather_network_resources': dict(choices=choices, type='list', elements='str'), + } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/interfaces/interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/interfaces/interfaces.py new file mode 100644 index 000000000..76c36a90b --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/interfaces/interfaces.py @@ -0,0 +1,56 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class InterfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "description": {"type": "str"}, + "enabled": {"type": "bool"}, + "mtu": {"type": "int"}, + "name": {"required": True, "type": "str"} + }, + "type": "list" + }, + "state": { + "choices": ["merged", "deleted"], + "default": "merged", + "type": "str" + } + } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_interfaces/l2_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_interfaces/l2_interfaces.py new file mode 100644 index 000000000..bbebe2d54 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_interfaces/l2_interfaces.py @@ -0,0 +1,71 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_l2_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class L2_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_l2_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'access': { + 'options': { + 'vlan': {'type': 'int'} + }, + 'type': 'dict' + }, + 'name': {'required': True, 'type': 'str'}, + 'trunk': { + 'options': { + 'allowed_vlans': { + 'elements': 'dict', + 'options': { + 'vlan': {'type': 'int'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_interfaces/l3_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_interfaces/l3_interfaces.py new file mode 100644 index 000000000..6e83289cc --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_interfaces/l3_interfaces.py @@ -0,0 +1,81 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_l3_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class L3_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_l3_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'ipv4': { + 'mutually_exclusive': [['addresses', 'anycast_addresses']], + 'options': { + 'addresses': { + 'elements': 'dict', + 'options': { + 'address': {'type': 'str'}, + 'secondary': {'default': 'False', 'type': 'bool'} + }, + 'type': 'list' + }, + 'anycast_addresses': {'elements': 'str', 'type': 'list'}, + }, + 'type': 'dict' + }, + 'ipv6': { + 'options': { + 'addresses': { + 'elements': 'dict', + 'options': { + 'address': {'type': 'str'} + }, + 'type': 'list' + }, + 'enabled': {'type': 'bool'} + }, + 'type': 'dict' + }, + 'name': {'required': True, 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lag_interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lag_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lag_interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lag_interfaces/lag_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lag_interfaces/lag_interfaces.py new file mode 100644 index 000000000..867d61a27 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lag_interfaces/lag_interfaces.py @@ -0,0 +1,67 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_lag_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Lag_interfacesArgs(object): # pylint: disable=R0903 + + """The arg spec for the sonic_lag_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "members": { + "options": { + "interfaces": { + "elements": "dict", + "options": { + "member": {"type": "str"} + }, + "type": "list" + } + }, + "type": "dict" + }, + "name": {"required": True, "type": "str"}, + "mode": {"type": "str", "choices": ["static", "lacp"]} + }, + "type": "list" + }, + "state": { + "choices": ["merged", "deleted"], + "default": "merged", + "type": "str" + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mclag/mclag.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mclag/mclag.py new file mode 100644 index 000000000..be3c38ca2 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mclag/mclag.py @@ -0,0 +1,82 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_mclag module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class MclagArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_mclag module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'domain_id': {'required': True, 'type': 'int'}, + 'keepalive': {'type': 'int'}, + 'peer_address': {'type': 'str'}, + 'peer_link': {'type': 'str'}, + 'members': { + 'options': { + 'portchannels': { + 'elements': 'dict', + 'options': { + 'lag': {'type': 'str'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'session_timeout': {'type': 'int'}, + 'source_address': {'type': 'str'}, + 'system_mac': {'type': 'str'}, + 'unique_ip': { + 'options': { + 'vlans': { + 'elements': 'dict', + 'options': { + 'vlan': {'type': 'str'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/ntp/ntp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/ntp/ntp.py new file mode 100644 index 000000000..062520af9 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/ntp/ntp.py @@ -0,0 +1,89 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 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_ntp module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class NtpArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_ntp module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'enable_ntp_auth': {'type': 'bool'}, + 'ntp_keys': { + 'elements': 'dict', + 'options': { + 'encrypted': {'type': 'bool'}, + 'key_id': {'required': True, + 'type': 'int', + 'no_log': True}, + 'key_type': {'type': 'str', + 'choices': ['NTP_AUTH_SHA1', + 'NTP_AUTH_MD5', + 'NTP_AUTH_SHA2_256']}, + 'key_value': {'type': 'str', 'no_log': True} + }, + 'type': 'list', + 'no_log': True + }, + 'servers': { + 'elements': 'dict', + 'options': { + 'address': {'required': True, + 'type': 'str'}, + 'key_id': {'type': 'int', 'no_log': True}, + 'maxpoll': {'type': 'int'}, + 'minpoll': {'type': 'int'} + }, + 'type': 'list' + }, + 'source_interfaces': { + 'elements': 'str', + 'type': 'list' + }, + 'trusted_keys': { + 'elements': 'int', + 'type': 'list', + 'no_log': True + }, + 'vrf': {'type': 'str'} + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_breakout/port_breakout.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_breakout/port_breakout.py new file mode 100644 index 000000000..3b8f4a5a3 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_breakout/port_breakout.py @@ -0,0 +1,57 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_port_breakout module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Port_breakoutArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_port_breakout module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'mode': { + 'choices': ['1x100G', '1x400G', '1x40G', '2x100G', '2x200G', + '2x50G', '4x100G', '4x10G', '4x25G', '4x50G'], + 'type': 'str' + }, + 'name': {'required': True, 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/prefix_lists/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/prefix_lists/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/prefix_lists/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/prefix_lists/prefix_lists.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/prefix_lists/prefix_lists.py new file mode 100644 index 000000000..d043ae6f8 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/prefix_lists/prefix_lists.py @@ -0,0 +1,71 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_prefix_lists module +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class Prefix_listsArgs: # pylint: disable=R0903 + """The arg spec for the sonic_prefix_lists module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'default': 'ipv4', + 'type': 'str' + }, + 'name': {'required': True, 'type': 'str'}, + 'prefixes': { + 'elements': 'dict', + 'options': { + 'action': { + 'choices': ['permit', 'deny'], + 'required': True, + 'type': 'str' + }, + 'ge': {'type': 'int'}, + 'le': {'type': 'int'}, + 'prefix': {'required': True, 'type': 'str'}, + 'sequence': {'required': True, 'type': 'int'}}, + 'type': 'list' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/radius_server/radius_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/radius_server/radius_server.py new file mode 100644 index 000000000..a56147a5b --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/radius_server/radius_server.py @@ -0,0 +1,83 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2021 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_radius_server module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Radius_serverArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_radius_server module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'auth_type': { + 'choices': ['pap', 'chap', 'mschapv2'], + 'default': 'pap', + 'type': 'str' + }, + 'key': {'type': 'str', 'no_log': True}, + 'nas_ip': {'type': 'str'}, + 'retransmit': {'type': 'int'}, + 'servers': { + 'options': { + 'host': { + 'elements': 'dict', + 'options': { + 'auth_type': { + 'choices': ['pap', 'chap', 'mschapv2'], + 'type': 'str' + }, + 'key': {'type': 'str', 'no_log': True}, + 'name': {'type': 'str'}, + 'port': {'type': 'int'}, + 'priority': {'type': 'int'}, + 'retransmit': {'type': 'int'}, + 'source_interface': {'type': 'str'}, + 'timeout': {'type': 'int'}, + 'vrf': {'type': 'str'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'statistics': {'type': 'bool'}, + 'timeout': {'type': 'int'} + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/static_routes/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/static_routes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/static_routes/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/static_routes/static_routes.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/static_routes/static_routes.py new file mode 100644 index 000000000..a146f1ecd --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/static_routes/static_routes.py @@ -0,0 +1,79 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 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_static_routes module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Static_routesArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_static_routes module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'static_list': { + 'elements': 'dict', + 'options': { + 'next_hops': { + 'elements': 'dict', + 'options': { + 'index': { + 'required': True, + 'options': { + 'blackhole': {'type': 'bool', 'default': False}, + 'interface': {'type': 'str'}, + 'nexthop_vrf': {'type': 'str'}, + 'next_hop': {'type': 'str'} + }, + 'type': 'dict' + }, + 'metric': {'type': 'int'}, + 'tag': {'type': 'int'}, + 'track': {'type': 'int'} + }, + 'type': 'list' + }, + 'prefix': {'required': True, 'type': 'str'} + }, + 'type': 'list' + }, + 'vrf_name': {'required': True, 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/system/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/system/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/system/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/system/system.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/system/system.py new file mode 100644 index 000000000..b08c5f4bc --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/system/system.py @@ -0,0 +1,64 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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_system module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class SystemArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_system module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'anycast_address': { + 'options': { + 'ipv4': {'type': 'bool'}, + 'ipv6': {'type': 'bool'}, + 'mac_address': {'type': 'str'} + }, + 'type': 'dict' + }, + 'hostname': {'type': 'str'}, + 'interface_naming': { + 'choices': ['standard', 'native'], + 'type': 'str' + } + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/tacacs_server/tacacs_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/tacacs_server/tacacs_server.py new file mode 100644 index 000000000..aad1746d4 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/tacacs_server/tacacs_server.py @@ -0,0 +1,80 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2021 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_tacacs_server module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Tacacs_serverArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_tacacs_server module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'auth_type': { + 'choices': ['pap', 'chap', 'mschap', 'login'], + 'default': 'pap', + 'type': 'str' + }, + 'key': {'type': 'str', 'no_log': True}, + 'servers': { + 'options': { + 'host': { + 'elements': 'dict', + 'options': { + 'auth_type': { + 'choices': ['pap', 'chap', 'mschap', 'login'], + 'default': 'pap', + 'type': 'str' + }, + 'key': {'type': 'str', 'no_log': True}, + 'name': {'type': 'str'}, + 'port': {'default': 49, 'type': 'int'}, + 'priority': {'default': 1, 'type': 'int'}, + 'timeout': {'default': 5, 'type': 'int'}, + 'vrf': {'default': 'default', 'type': 'str'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'source_interface': {'type': 'str'}, + 'timeout': {'type': 'int'} + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/users/users.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/users/users.py new file mode 100644 index 000000000..db23d78e0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/users/users.py @@ -0,0 +1,62 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_users module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class UsersArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_users module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'name': {'required': True, 'type': 'str'}, + 'password': {'type': 'str', 'no_log': True}, + 'role': { + 'choices': ['admin', 'operator'], + 'type': 'str' + }, + 'update_password': { + 'choices': ['always', 'on_create'], + 'default': 'always', + 'type': 'str' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlans/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlans/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlans/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlans/vlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlans/vlans.py new file mode 100644 index 000000000..971fc8571 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlans/vlans.py @@ -0,0 +1,54 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_vlans module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class VlansArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_vlans module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'vlan_id': {'required': True, 'type': 'int'}, + 'description': {'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vrfs/vrfs.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vrfs/vrfs.py new file mode 100644 index 000000000..e074936a7 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vrfs/vrfs.py @@ -0,0 +1,66 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_vrfs module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class VrfsArgs(object): # pylint: disable=R0903 + + """The arg spec for the sonic_vrfs module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "elements": "dict", + "options": { + "members": { + "options": { + "interfaces": { + "elements": "dict", + "options": { + "name": {"type": "str"} + }, + "type": "list" + } + }, + "type": "dict" + }, + "name": {"required": True, "type": "str"} + }, + "type": "list" + }, + "state": { + "choices": ["merged", "deleted"], + "default": "merged", + "type": "str" + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vxlans/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vxlans/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vxlans/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vxlans/vxlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vxlans/vxlans.py new file mode 100644 index 000000000..dd475b78a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vxlans/vxlans.py @@ -0,0 +1,73 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_vxlans module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class VxlansArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_vxlans module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'evpn_nvo': {'type': 'str'}, + 'name': {'required': True, 'type': 'str'}, + 'source_ip': {'type': 'str'}, + 'primary_ip': {'type': 'str'}, + 'vlan_map': { + 'elements': 'dict', + 'options': { + 'vlan': {'type': 'int'}, + 'vni': {'required': True, 'type': 'int'} + }, + 'type': 'list' + }, + 'vrf_map': { + 'elements': 'dict', + 'options': { + 'vni': {'required': True, 'type': 'int'}, + 'vrf': {'type': 'str'} + }, + 'type': 'list' + } + }, + 'required_together': [['source_ip', 'evpn_nvo']], + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/aaa/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/aaa/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/aaa/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/aaa/aaa.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/aaa/aaa.py new file mode 100644 index 000000000..85f93bc73 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/aaa/aaa.py @@ -0,0 +1,236 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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_aaa 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 ( + update_states, + get_diff, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + +PATCH = 'patch' +DELETE = 'delete' + + +class Aaa(ConfigBase): + """ + The sonic_aaa class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'aaa', + ] + + def __init__(self, module): + super(Aaa, self).__init__(module) + + def get_aaa_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) + aaa_facts = facts['ansible_network_resources'].get('aaa') + if not aaa_facts: + return [] + return aaa_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_aaa_facts = self.get_aaa_facts() + commands, requests = self.set_config(existing_aaa_facts) + if commands and len(requests) > 0: + if not self._module.check_mode: + self.edit_config(requests) + result['changed'] = True + result['commands'] = commands + + changed_aaa_facts = self.get_aaa_facts() + + result['before'] = existing_aaa_facts + if result['changed']: + result['after'] = changed_aaa_facts + + result['warnings'] = warnings + return result + + def edit_config(self, requests): + try: + response = edit_config(self._module, to_request(self._module, requests)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + def set_config(self, existing_aaa_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_aaa_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 not want: + want = {} + + if state == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged': + diff = get_diff(want, have) + commands = self._state_merged(want, have, diff) + return commands + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + requests = [] + if diff: + requests = self.get_create_aaa_request(diff) + if len(requests) > 0: + commands = update_states(diff, "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 = [] + if not want: + if have: + requests = self.get_delete_all_aaa_request(have) + if len(requests) > 0: + commands = update_states(have, "deleted") + else: + want = utils.remove_empties(want) + new_have = self.remove_default_entries(have) + d_diff = get_diff(want, new_have, is_skeleton=True) + diff_want = get_diff(want, d_diff, is_skeleton=True) + if diff_want: + requests = self.get_delete_all_aaa_request(diff_want) + if len(requests) > 0: + commands = update_states(diff_want, "deleted") + return commands, requests + + def get_create_aaa_request(self, commands): + requests = [] + aaa_path = 'data/openconfig-system:system/aaa' + method = PATCH + aaa_payload = self.build_create_aaa_payload(commands) + if aaa_payload: + request = {'path': aaa_path, 'method': method, 'data': aaa_payload} + requests.append(request) + return requests + + def build_create_aaa_payload(self, commands): + payload = {} + if "authentication" in commands and commands["authentication"]: + payload = {"openconfig-system:aaa": {"authentication": {"config": {"authentication-method": []}}}} + if "local" in commands["authentication"]["data"] and commands["authentication"]["data"]["local"]: + payload['openconfig-system:aaa']['authentication']['config']['authentication-method'].append("local") + if "group" in commands["authentication"]["data"] and commands["authentication"]["data"]["group"]: + auth_method = commands["authentication"]["data"]["group"] + payload['openconfig-system:aaa']['authentication']['config']['authentication-method'].append(auth_method) + if "fail_through" in commands["authentication"]["data"]: + cfg = {'failthrough': str(commands["authentication"]["data"]["fail_through"])} + payload['openconfig-system:aaa']['authentication']['config'].update(cfg) + return payload + + def remove_default_entries(self, data): + new_data = {} + if not data: + return new_data + else: + new_data = {'authentication': {'data': {}}} + local = data['authentication']['data'].get('local', None) + if local is not None: + new_data["authentication"]["data"]["local"] = local + group = data['authentication']['data'].get('group', None) + if group is not None: + new_data["authentication"]["data"]["group"] = group + fail_through = data['authentication']['data'].get('fail_through', None) + if fail_through is not None: + new_data["authentication"]["data"]["fail_through"] = fail_through + return new_data + + def get_delete_all_aaa_request(self, have): + requests = [] + if "authentication" in have and have["authentication"]: + if "local" in have["authentication"]["data"] or "group" in have["authentication"]["data"]: + request = self.get_authentication_method_delete_request() + requests.append(request) + if "fail_through" in have["authentication"]["data"]: + request = self.get_failthrough_delete_request() + requests.append(request) + return requests + + def get_authentication_method_delete_request(self): + path = 'data/openconfig-system:system/aaa/authentication/config/authentication-method' + method = DELETE + request = {'path': path, 'method': method} + return request + + def get_failthrough_delete_request(self): + path = 'data/openconfig-system:system/aaa/authentication/config/failthrough' + method = DELETE + request = {'path': path, 'method': method} + return request diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp/bgp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp/bgp.py new file mode 100644 index 000000000..fd4d5c57e --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp/bgp.py @@ -0,0 +1,598 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_bgp 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 + +try: + from urllib import quote +except ImportError: + from urllib.parse import quote + +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, + search_obj_in_list, + remove_empties +) +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + dict_to_set, + update_states, + get_diff, + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import to_request +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +POST = 'post' +DELETE = 'delete' +PUT = 'put' + +TEST_KEYS = [{'config': {'vrf_name': '', 'bgp_as': ''}}] + + +class Bgp(ConfigBase): + """ + The sonic_bgp class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bgp', + ] + + network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' + protocol_bgp_path = 'protocols/protocol=BGP,bgp/bgp' + log_neighbor_changes_path = 'logging-options/config/log-neighbor-state-changes' + holdtime_path = 'config/hold-time' + keepalive_path = 'config/keepalive-interval' + + def __init__(self, module): + super(Bgp, self).__init__(module) + + def get_bgp_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) + bgp_facts = facts['ansible_network_resources'].get('bgp') + if not bgp_facts: + bgp_facts = [] + return bgp_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_bgp_facts = self.get_bgp_facts() + commands, requests = self.set_config(existing_bgp_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_bgp_facts = self.get_bgp_facts() + + result['before'] = existing_bgp_facts + if result['changed']: + result['after'] = changed_bgp_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bgp_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_bgp_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_bgp_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + is_delete_all = False + # if want is none, then delete all the bgps + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests = self.get_delete_bgp_requests(commands, have, is_delete_all) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_delete_single_bgp_request(self, vrf_name): + delete_path = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + return ({'path': delete_path, 'method': DELETE}) + + def get_delete_max_med_requests(self, vrf_name, max_med, match): + requests = [] + + match_max_med = match.get('max_med', None) + if not max_med or not match_max_med: + return requests + + generic_del_path = '%s=%s/%s/global/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + + match_max_med_on_startup = match.get('max_med', {}).get('on_startup') + if match_max_med_on_startup: + requests.append({'path': generic_del_path + "max-med/config/time", 'method': DELETE}) + requests.append({'path': generic_del_path + "max-med/config/max-med-val", 'method': DELETE}) + + return requests + + def get_delete_bestpath_requests(self, vrf_name, bestpath, match): + requests = [] + + match_bestpath = match.get('bestpath', None) + if not bestpath or not match_bestpath: + return requests + + route_selection_del_path = '%s=%s/%s/global/route-selection-options/config/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + multi_paths_del_path = '%s=%s/%s/global/use-multiple-paths/ebgp/config/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + generic_del_path = '%s=%s/%s/global/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + + if bestpath.get('compare_routerid', None) and match_bestpath.get('compare_routerid', None): + url = '%s=%s/%s/global/route-selection-options' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + route_selection_cfg = {} + route_selection_cfg['external-compare-router-id'] = False + payload = {'route-selection-options': {'config': route_selection_cfg}} + requests.append({'path': url, 'data': payload, 'method': PATCH}) + # requests.append({'path': route_selection_del_path + "external-compare-router-id", 'method': DELETE}) + + match_as_path = match_bestpath.get('as_path', None) + as_path = bestpath.get('as_path', None) + if as_path and match_as_path: + if as_path.get('confed', None) is not None and match_as_path.get('confed', None): + requests.append({'path': route_selection_del_path + "compare-confed-as-path", 'method': DELETE}) + if as_path.get('ignore', None) is not None and match_as_path.get('ignore', None): + requests.append({'path': route_selection_del_path + "ignore-as-path-length", 'method': DELETE}) + if as_path.get('multipath_relax', None) is not None and match_as_path.get('multipath_relax', None): + requests.append({'path': multi_paths_del_path + "allow-multiple-as", 'method': DELETE}) + if as_path.get('multipath_relax_as_set', None) is not None and match_as_path.get('multipath_relax_as_set', None): + requests.append({'path': multi_paths_del_path + "as-set", 'method': DELETE}) + + match_med = match_bestpath.get('med', None) + med = bestpath.get('med', None) + if med and match_med: + if med.get('confed', None) is not None and match_med.get('confed', None): + requests.append({'path': route_selection_del_path + "med-confed", 'method': DELETE}) + if med.get('missing_as_worst', None) is not None and match_med.get('missing_as_worst', None): + requests.append({'path': route_selection_del_path + "med-missing-as-worst", 'method': DELETE}) + if med.get('always_compare_med', None) is not None and match_med.get('always_compare_med', None): + requests.append({'path': route_selection_del_path + "always-compare-med", 'method': DELETE}) + if med.get('max_med_val', None) is not None and match_med.get('max_med_val', None): + requests.append({'path': generic_del_path + "max-med/config/admin-max-med-val", 'method': DELETE}) + + return requests + + def get_delete_all_bgp_requests(self, commands): + requests = [] + for cmd in commands: + requests.append(self.get_delete_single_bgp_request(cmd['vrf_name'])) + return requests + + def get_delete_specific_bgp_param_request(self, command, match): + vrf_name = command['vrf_name'] + requests = [] + + router_id = command.get('router_id', None) + timers = command.get('timers', None) + holdtime = None + keepalive = None + if timers: + holdtime = command['timers'].get('holdtime', None) + keepalive = command['timers'].get('keepalive_interval', None) + log_neighbor_changes = command.get('log_neighbor_changes', None) + bestpath = command.get('bestpath', None) + + if router_id and match.get('router_id', None): + url = '%s=%s/%s/global/config/router-id' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + requests.append({"path": url, "method": DELETE}) + + if holdtime and match['timers'].get('holdtime', None) != 180: + url = '%s=%s/%s/global/config/hold-time' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + requests.append({"path": url, "method": DELETE}) + + if keepalive and match['timers'].get('keepalive_interval', None) != 60: + url = '%s=%s/%s/global/config/keepalive-interval' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + requests.append({"path": url, "method": DELETE}) + + # Delete the log_neighbor_changes only when existing values is True. + if log_neighbor_changes is not None and match.get('log_neighbor_changes', None): + del_log_neighbor_req = self.get_modify_log_change_request(vrf_name, False) + if del_log_neighbor_req: + requests.append(del_log_neighbor_req) + + bestpath_del_reqs = self.get_delete_bestpath_requests(vrf_name, bestpath, match) + if bestpath_del_reqs: + requests.extend(bestpath_del_reqs) + + max_med = command.get('max_med', None) + max_med_del_reqs = self.get_delete_max_med_requests(vrf_name, max_med, match) + if max_med_del_reqs: + requests.extend(max_med_del_reqs) + + return requests + + def get_delete_bgp_requests(self, commands, have, is_delete_all): + requests = [] + if is_delete_all: + requests = self.get_delete_all_bgp_requests(commands) + else: + for cmd in commands: + vrf_name = cmd['vrf_name'] + as_val = cmd['bgp_as'] + + match = next((cfg for cfg in have if cfg['vrf_name'] == vrf_name and cfg['bgp_as'] == as_val), None) + if not match: + continue + # if there is specific parameters to delete then delete those alone + if cmd.get('router_id', None) or cmd.get('log_neighbor_changes', None) or cmd.get('bestpath', None): + requests.extend(self.get_delete_specific_bgp_param_request(cmd, match)) + else: + # delete entire bgp + requests.append(self.get_delete_single_bgp_request(vrf_name)) + + if requests: + # reorder the requests to get default vrfs at end of the requests. so deletion will get success + default_vrf_reqs = [] + other_vrf_reqs = [] + for req in requests: + if '=default/' in req['path']: + default_vrf_reqs.append(req) + else: + other_vrf_reqs.append(req) + requests.clear() + requests.extend(other_vrf_reqs) + requests.extend(default_vrf_reqs) + + return requests + + def get_modify_multi_paths_req(self, vrf_name, as_path): + request = None + if not as_path: + return request + + method = PATCH + multipath_cfg = {} + + as_path_multipath_relax = as_path.get('multipath_relax', None) + as_path_multipath_relax_as_set = as_path.get('multipath_relax_as_set', None) + + if as_path_multipath_relax is not None: + multipath_cfg['allow-multiple-as'] = as_path_multipath_relax + if as_path_multipath_relax_as_set is not None: + multipath_cfg['as-set'] = as_path_multipath_relax_as_set + + payload = {"openconfig-network-instance:config": multipath_cfg} + if payload: + url = '%s=%s/%s/global/use-multiple-paths/ebgp/config' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + request = {"path": url, "method": method, "data": payload} + + return request + + def get_modify_route_selection_req(self, vrf_name, compare_routerid, as_path, med): + requests = [] + if compare_routerid is None and not as_path and not med: + return requests + + route_selection_cfg = {} + + as_path_confed = None + as_path_ignore = None + + med_confed = None + med_missing_as_worst = None + always_compare_med = None + + if compare_routerid is not None: + route_selection_cfg['external-compare-router-id'] = compare_routerid + + if as_path: + as_path_confed = as_path.get('confed', None) + as_path_ignore = as_path.get('ignore', None) + if as_path_confed is not None: + route_selection_cfg['compare-confed-as-path'] = as_path_confed + if as_path_ignore is not None: + route_selection_cfg['ignore-as-path-length'] = as_path_ignore + + if med: + med_confed = med.get('confed', None) + med_missing_as_worst = med.get('missing_as_worst', None) + always_compare_med = med.get('always_compare_med', None) + if med_confed is not None: + route_selection_cfg['med-confed'] = med_confed + if med_missing_as_worst is not None: + route_selection_cfg['med-missing-as-worst'] = med_missing_as_worst + if always_compare_med is not None: + route_selection_cfg['always-compare-med'] = always_compare_med + method = PATCH + payload = {'route-selection-options': {'config': route_selection_cfg}} + + if payload: + url = '%s=%s/%s/global/route-selection-options' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_modify_bestpath_requests(self, vrf_name, bestpath): + requests = [] + if not bestpath: + return requests + + compare_routerid = bestpath.get('compare_routerid', None) + as_path = bestpath.get('as_path', None) + med = bestpath.get('med', None) + + route_selection_req = self.get_modify_route_selection_req(vrf_name, compare_routerid, as_path, med) + if route_selection_req: + requests.extend(route_selection_req) + + multi_paths_req = self.get_modify_multi_paths_req(vrf_name, as_path) + if multi_paths_req: + requests.append(multi_paths_req) + + return requests + + def get_modify_max_med_requests(self, vrf_name, max_med): + request = None + method = PATCH + payload = {} + on_startup_time = max_med.get('on_startup', {}).get('timer') + on_startup_med = max_med.get('on_startup', {}).get('med_val') + + if on_startup_med is not None: + payload = { + 'max-med': { + 'config': { + 'max-med-val': on_startup_med, + 'time': on_startup_time + } + } + } + + if payload: + url = '%s=%s/%s/global/max-med' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + request = {"path": url, "method": method, "data": payload} + + return [request] + + def get_modify_log_change_request(self, vrf_name, log_neighbor_changes): + request = None + method = PATCH + payload = {} + + if log_neighbor_changes is not None: + payload['log-neighbor-state-changes'] = log_neighbor_changes + + if payload: + url = '%s=%s/%s/global/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.log_neighbor_changes_path) + request = {"path": url, "method": method, "data": payload} + + return request + + def get_modify_holdtime_request(self, vrf_name, holdtime): + request = None + method = PATCH + payload = {} + + if holdtime is not None: + payload['hold-time'] = str(holdtime) + + if payload: + url = '%s=%s/%s/global/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.holdtime_path) + request = {"path": url, "method": method, "data": payload} + + return request + + def get_modify_keepalive_request(self, vrf_name, keepalive_interval): + request = None + method = PATCH + payload = {} + + if keepalive_interval is not None: + payload['keepalive-interval'] = str(keepalive_interval) + + if payload: + url = '%s=%s/%s/global/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.keepalive_path) + request = {"path": url, "method": method, "data": payload} + + return request + + def get_new_bgp_request(self, vrf_name, as_val): + request = None + url = None + method = PATCH + payload = {} + + cfg = {} + if as_val: + as_cfg = {'config': {'as': float(as_val)}} + global_cfg = {'global': as_cfg} + cfg = {'bgp': global_cfg} + cfg['name'] = "bgp" + cfg['identifier'] = "openconfig-policy-types:BGP" + + if cfg: + payload['openconfig-network-instance:protocol'] = [cfg] + url = '%s=%s/protocols/protocol/' % (self.network_instance_path, vrf_name) + request = {"path": url, "method": method, "data": payload} + + return request + + def get_modify_global_config_request(self, vrf_name, router_id, as_val): + request = None + method = PATCH + payload = {} + + cfg = {} + if router_id: + cfg['router-id'] = router_id + if as_val: + cfg['as'] = float(as_val) + + if cfg: + payload['openconfig-network-instance:config'] = cfg + url = '%s=%s/%s/global/config' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + request = {"path": url, "method": method, "data": payload} + + return request + + def get_modify_bgp_requests(self, commands, have): + requests = [] + if not commands: + return requests + + # Create URL and payload + for conf in commands: + vrf_name = conf['vrf_name'] + as_val = None + router_id = None + log_neighbor_changes = None + bestpath = None + max_med = None + holdtime = None + keepalive_interval = None + + if 'bgp_as' in conf: + as_val = conf['bgp_as'] + if 'router_id' in conf: + router_id = conf['router_id'] + if 'log_neighbor_changes' in conf: + log_neighbor_changes = conf['log_neighbor_changes'] + if 'bestpath' in conf: + bestpath = conf['bestpath'] + if 'max_med' in conf: + max_med = conf['max_med'] + if 'timers' in conf and conf['timers']: + if 'holdtime' in conf['timers']: + holdtime = conf['timers']['holdtime'] + if 'keepalive_interval' in conf['timers']: + keepalive_interval = conf['timers']['keepalive_interval'] + + if not any(cfg for cfg in have if cfg['vrf_name'] == vrf_name and (cfg['bgp_as'] == as_val)): + new_bgp_req = self.get_new_bgp_request(vrf_name, as_val) + if new_bgp_req: + requests.append(new_bgp_req) + + global_req = self.get_modify_global_config_request(vrf_name, router_id, as_val) + if global_req: + requests.append(global_req) + + log_neighbor_changes_req = self.get_modify_log_change_request(vrf_name, log_neighbor_changes) + if log_neighbor_changes_req: + requests.append(log_neighbor_changes_req) + + if holdtime: + holdtime_req = self.get_modify_holdtime_request(vrf_name, holdtime) + if holdtime_req: + requests.append(holdtime_req) + + if keepalive_interval: + keepalive_req = self.get_modify_keepalive_request(vrf_name, keepalive_interval) + if keepalive_req: + requests.append(keepalive_req) + + bestpath_reqs = self.get_modify_bestpath_requests(vrf_name, bestpath) + if bestpath_reqs: + requests.extend(bestpath_reqs) + if max_med: + max_med_reqs = self.get_modify_max_med_requests(vrf_name, max_med) + if max_med_reqs: + requests.extend(max_med_reqs) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_af/bgp_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_af/bgp_af.py new file mode 100644 index 000000000..2a5c4cfca --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_af/bgp_af.py @@ -0,0 +1,848 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_bgp_af 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 + +try: + from urllib import quote +except ImportError: + from urllib.parse import quote + +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, + search_obj_in_list, + remove_empties +) +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + dict_to_set, + update_states, + get_diff, + remove_empties_from_list, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import to_request +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( + validate_bgps, +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'config': {'vrf_name': '', 'bgp_as': ''}}, + {'afis': {'afi': '', 'safi': ''}}, + {'redistribute': {'protocol': ''}}, + {'route_advertise_list': {'advertise_afi': ''}} +] + + +class Bgp_af(ConfigBase): + """ + The sonic_bgp_af class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bgp_af', + ] + + network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' + protocol_bgp_path = 'protocols/protocol=BGP,bgp/bgp' + l2vpn_evpn_config_path = 'l2vpn-evpn/openconfig-bgp-evpn-ext:config' + l2vpn_evpn_route_advertise_path = 'l2vpn-evpn/openconfig-bgp-evpn-ext:route-advertise' + afi_safi_path = 'global/afi-safis/afi-safi' + table_connection_path = 'table-connections/table-connection' + + def __init__(self, module): + super(Bgp_af, self).__init__(module) + + def get_bgp_af_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) + bgp_af_facts = facts['ansible_network_resources'].get('bgp_af') + if not bgp_af_facts: + bgp_af_facts = [] + return bgp_af_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_bgp_af_facts = self.get_bgp_af_facts() + commands, requests = self.set_config(existing_bgp_af_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_bgp_af_facts = self.get_bgp_af_facts() + + result['before'] = existing_bgp_af_facts + if result['changed']: + result['after'] = changed_bgp_af_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bgp_af_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_bgp_af_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + validate_bgps(self._module, commands, have) + requests = self.get_modify_bgp_af_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the bgp_afs + is_delete_all = False + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests = self.get_delete_bgp_af_requests(commands, have, is_delete_all) + requests.extend(self.get_delete_route_advertise_requests(commands, have, is_delete_all)) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + return commands, requests + + def get_modify_address_family_request(self, vrf_name, conf_afi, conf_safi): + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s/global' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + afi_safi_load = {'afi-safi-name': ("openconfig-bgp-types:%s" % (afi_safi))} + afi_safis_load = {'afi-safis': {'afi-safi': [afi_safi_load]}} + pay_load = {'openconfig-network-instance:global': afi_safis_load} + + return ({"path": url, "method": PATCH, "data": pay_load}) + + def get_modify_advertise_request(self, vrf_name, conf_afi, conf_safi, conf_addr_fam): + request = None + conf_adv_pip = conf_addr_fam.get('advertise_pip', None) + conf_adv_pip_ip = conf_addr_fam.get('advertise_pip_ip', None) + conf_adv_pip_peer_ip = conf_addr_fam.get('advertise_pip_peer_ip', None) + conf_adv_svi_ip = conf_addr_fam.get('advertise_svi_ip', None) + conf_adv_all_vni = conf_addr_fam.get('advertise_all_vni', None) + conf_adv_default_gw = conf_addr_fam.get('advertise_default_gw', None) + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + evpn_cfg = {} + + if conf_adv_pip: + evpn_cfg['advertise-pip'] = conf_adv_pip + + if conf_adv_pip_ip: + evpn_cfg['advertise-pip-ip'] = conf_adv_pip_ip + + if conf_adv_pip_peer_ip: + evpn_cfg['advertise-pip-peer-ip'] = conf_adv_pip_peer_ip + + if conf_adv_svi_ip: + evpn_cfg['advertise-svi-ip'] = conf_adv_svi_ip + + if conf_adv_all_vni: + evpn_cfg['advertise-all-vni'] = conf_adv_all_vni + + if conf_adv_default_gw: + evpn_cfg['advertise-default-gw'] = conf_adv_default_gw + + if evpn_cfg: + url = '%s=%s/%s/global' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + afi_safi_load = {'afi-safi-name': ("openconfig-bgp-types:%s" % (afi_safi))} + afi_safi_load['l2vpn-evpn'] = {'openconfig-bgp-evpn-ext:config': evpn_cfg} + afi_safis_load = {'afi-safis': {'afi-safi': [afi_safi_load]}} + pay_load = {'openconfig-network-instance:global': afi_safis_load} + request = {"path": url, "method": PATCH, "data": pay_load} + + return request + + def get_modify_route_advertise_list_request(self, vrf_name, conf_afi, conf_safi, conf_addr_fam): + request = [] + route_advertise = [] + afi_safi = ('%s_%s' % (conf_afi, conf_safi)).upper() + route_advertise_list = conf_addr_fam.get('route_advertise_list', []) + if route_advertise_list: + for rt_adv in route_advertise_list: + advertise_afi = rt_adv.get('advertise_afi', None) + route_map = rt_adv.get('route_map', None) + if advertise_afi: + advertise_afi_safi = '%s_UNICAST' % advertise_afi.upper() + url = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '/%s=%s/%s' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_route_advertise_path) + cfg = None + if route_map: + route_map_list = [route_map] + cfg = {'advertise-afi-safi': advertise_afi_safi, 'route-map': route_map_list} + else: + cfg = {'advertise-afi-safi': advertise_afi_safi} + route_advertise.append({'advertise-afi-safi': advertise_afi_safi, 'config': cfg}) + pay_load = {'openconfig-bgp-evpn-ext:route-advertise': {'route-advertise-list': route_advertise}} + request = {"path": url, "method": PATCH, "data": pay_load} + return request + + def get_modify_redistribute_requests(self, vrf_name, conf_afi, conf_safi, conf_redis_arr): + requests = [] + url = "%s=%s/table-connections" % (self.network_instance_path, vrf_name) + cfgs = [] + for conf_redis in conf_redis_arr: + conf_metric = conf_redis.get('metric', None) + if conf_metric is not None: + conf_metric = float(conf_redis['metric']) + + afi_cfg = "openconfig-types:%s" % (conf_afi.upper()) + cfg_data = {'address-family': afi_cfg} + cfg_data['dst-protocol'] = "openconfig-policy-types:BGP" + conf_protocol = conf_redis['protocol'].upper() + if conf_protocol == 'CONNECTED': + conf_protocol = "DIRECTLY_CONNECTED" + cfg_data['src-protocol'] = "openconfig-policy-types:%s" % (conf_protocol) + cfg_data['config'] = {'address-family': afi_cfg} + if conf_metric is not None: + cfg_data['config']['metric'] = conf_metric + + conf_route_map = conf_redis.get('route_map', None) + if conf_route_map: + cfg_data['config']['import-policy'] = [conf_route_map] + + cfgs.append(cfg_data) + + if cfgs: + pay_load = {'openconfig-network-instance:table-connections': {'table-connection': cfgs}} + requests.append({"path": url, "method": PATCH, "data": pay_load}) + return requests + + def get_modify_max_path_request(self, vrf_name, conf_afi, conf_safi, conf_max_path): + request = None + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '%s=%s/use-multiple-paths' % (self.afi_safi_path, afi_safi) + conf_ebgp = conf_max_path.get('ebgp', None) + conf_ibgp = conf_max_path.get('ibgp', None) + max_path_load = {} + if conf_ebgp: + max_path_load['ebgp'] = {'config': {'maximum-paths': conf_ebgp}} + if conf_ibgp: + max_path_load['ibgp'] = {'config': {'maximum-paths': conf_ibgp}} + + pay_load = {} + if max_path_load: + pay_load['openconfig-network-instance:use-multiple-paths'] = max_path_load + + request = {"path": url, "method": PATCH, "data": pay_load} + return request + + def get_modify_network_request(self, vrf_name, conf_afi, conf_safi, conf_network): + request = None + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '%s=%s/network-config' % (self.afi_safi_path, afi_safi) + network_payload = [] + for each in conf_network: + payload = {} + payload = {'config': {'prefix': each}, 'prefix': each} + network_payload.append(payload) + if network_payload: + new_payload = {'network-config': {'network': network_payload}} + + request = {"path": url, "method": PATCH, "data": new_payload} + return request + + def get_modify_dampening_request(self, vrf_name, conf_afi, conf_safi, conf_dampening): + request = None + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '%s=%s/route-flap-damping' % (self.afi_safi_path, afi_safi) + damp_payload = {'route-flap-damping': {'config': {'enabled': conf_dampening}}} + if damp_payload: + request = {"path": url, "method": PATCH, "data": damp_payload} + return request + + def get_modify_single_af_request(self, vrf_name, conf_afi, conf_safi, conf_addr_fam): + requests = [] + + requests.append(self.get_modify_address_family_request(vrf_name, conf_afi, conf_safi)) + if conf_afi == 'ipv4' and conf_safi == 'unicast': + conf_dampening = conf_addr_fam.get('dampening', None) + if conf_dampening: + request = self.get_modify_dampening_request(vrf_name, conf_afi, conf_safi, conf_dampening) + if request: + requests.append(request) + if conf_afi in ['ipv4', 'ipv6'] and conf_safi == 'unicast': + conf_redis_arr = conf_addr_fam.get('redistribute', []) + if conf_redis_arr: + requests.extend(self.get_modify_redistribute_requests(vrf_name, conf_afi, conf_safi, conf_redis_arr)) + conf_max_path = conf_addr_fam.get('max_path', None) + if conf_max_path: + request = self.get_modify_max_path_request(vrf_name, conf_afi, conf_safi, conf_max_path) + if request: + requests.append(request) + conf_network = conf_addr_fam.get('network', []) + if conf_network: + request = self.get_modify_network_request(vrf_name, conf_afi, conf_safi, conf_network) + if request: + requests.append(request) + elif conf_afi == "l2vpn" and conf_safi == 'evpn': + adv_req = self.get_modify_advertise_request(vrf_name, conf_afi, conf_safi, conf_addr_fam) + if adv_req: + requests.append(adv_req) + return requests + + def get_modify_all_af_requests(self, conf_addr_fams, vrf_name): + requests = [] + for conf_addr_fam in conf_addr_fams: + conf_afi = conf_addr_fam.get('afi', None) + conf_safi = conf_addr_fam.get('safi', None) + if conf_afi and conf_safi: + requests.extend(self.get_modify_single_af_request(vrf_name, conf_afi, conf_safi, conf_addr_fam)) + return requests + + def get_modify_requests(self, conf, match, vrf_name): + requests = [] + payload = {} + conf_addr_fams = conf.get('address_family', None) + if conf_addr_fams: + conf_addr_fams = conf_addr_fams.get('afis', []) + + mat_addr_fams = [] + if match: + mat_addr_fams = match.get('address_family', None) + if mat_addr_fams: + mat_addr_fams = mat_addr_fams.get('afis', []) + + if conf_addr_fams and not mat_addr_fams: + requests.extend(self.get_modify_all_af_requests(conf_addr_fams, vrf_name)) + else: + for conf_addr_fam in conf_addr_fams: + conf_afi = conf_addr_fam.get('afi', None) + conf_safi = conf_addr_fam.get('safi', None) + + if conf_afi is None or conf_safi is None: + continue + + mat_addr_fam = next((e_addr_fam for e_addr_fam in mat_addr_fams if (e_addr_fam['afi'] == conf_afi and e_addr_fam['safi'] == conf_safi)), None) + + if mat_addr_fam is None: + requests.extend(self.get_modify_single_af_request(vrf_name, conf_afi, conf_safi, conf_addr_fam)) + continue + + if conf_afi == 'ipv4' and conf_safi == 'unicast': + conf_dampening = conf_addr_fam.get('dampening', None) + if conf_dampening: + request = self.get_modify_dampening_request(vrf_name, conf_afi, conf_safi, conf_dampening) + if request: + requests.append(request) + + if conf_afi == "l2vpn" and conf_safi == "evpn": + adv_req = self.get_modify_advertise_request(vrf_name, conf_afi, conf_safi, conf_addr_fam) + rt_adv_req = self.get_modify_route_advertise_list_request(vrf_name, conf_afi, conf_safi, conf_addr_fam) + if adv_req: + requests.append(adv_req) + if rt_adv_req: + requests.append(rt_adv_req) + + elif conf_afi in ["ipv4", "ipv6"] and conf_safi == "unicast": + conf_redis_arr = conf_addr_fam.get('redistribute', []) + conf_max_path = conf_addr_fam.get('max_path', None) + conf_network = conf_addr_fam.get('network', []) + if not conf_redis_arr and not conf_max_path and not conf_network: + continue + + url = "%s=%s/table-connections" % (self.network_instance_path, vrf_name) + pay_loads = [] + modify_redis_arr = [] + for conf_redis in conf_redis_arr: + conf_metric = conf_redis.get('metric', None) + if conf_metric is not None: + conf_metric = float(conf_redis['metric']) + + conf_route_map = conf_redis.get('route_map', None) + + have_redis_arr = mat_addr_fam.get('redistribute', []) + have_redis = None + have_route_map = None + # Check the route_map, if existing route_map is different from required route_map, delete the existing route map + if conf_route_map and have_redis_arr: + have_redis = next((redis_cfg for redis_cfg in have_redis_arr if conf_redis['protocol'] == redis_cfg['protocol']), None) + if have_redis: + have_route_map = have_redis.get('route_map', None) + if have_route_map and have_route_map != conf_route_map: + requests.append(self.get_delete_route_map_request(vrf_name, conf_afi, have_redis, have_route_map)) + + modify_redis = {} + if conf_metric is not None: + modify_redis['metric'] = conf_metric + if conf_route_map: + modify_redis['route_map'] = conf_route_map + + if modify_redis: + modify_redis['protocol'] = conf_redis['protocol'] + modify_redis_arr.append(modify_redis) + + if modify_redis_arr: + requests.extend(self.get_modify_redistribute_requests(vrf_name, conf_afi, conf_safi, modify_redis_arr)) + if conf_max_path: + max_path_req = self.get_modify_max_path_request(vrf_name, conf_afi, conf_safi, conf_max_path) + if max_path_req: + requests.append(max_path_req) + + if conf_network: + network_req = self.get_modify_network_request(vrf_name, conf_afi, conf_safi, conf_network) + if network_req: + requests.append(network_req) + + return requests + + def get_modify_bgp_af_requests(self, commands, have): + requests = [] + if not commands: + return requests + + # Create URL and payload + for conf in commands: + vrf_name = conf['vrf_name'] + as_val = conf['bgp_as'] + + match = next((cfg for cfg in have if (cfg['vrf_name'] == vrf_name and (cfg['bgp_as'] == as_val))), None) + modify_reqs = self.get_modify_requests(conf, match, vrf_name) + if modify_reqs: + requests.extend(modify_reqs) + + return requests + + def get_delete_advertise_attribute_request(self, vrf_name, conf_afi, conf_safi, attr): + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '/%s=%s/%s/%s' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_config_path, attr) + + return ({"path": url, "method": DELETE}) + + def get_delete_route_advertise_request(self, vrf_name, conf_afi, conf_safi): + afi_safi = ('%s_%s' % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '/%s=%s/%s' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_route_advertise_path) + + return ({'path': url, 'method': DELETE}) + + def get_delete_route_advertise_list_request(self, vrf_name, conf_afi, conf_safi, advertise_afi): + afi_safi = ('%s_%s' % (conf_afi, conf_safi)).upper() + advertise_afi_safi = '%s_UNICAST' % advertise_afi.upper() + url = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '/%s=%s/%s/route-advertise-list=%s' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_route_advertise_path, advertise_afi_safi) + + return ({'path': url, 'method': DELETE}) + + def get_delete_route_advertise_route_map_request(self, vrf_name, conf_afi, conf_safi, advertise_afi, route_map): + afi_safi = ('%s_%s' % (conf_afi, conf_safi)).upper() + advertise_afi_safi = '%s_UNICAST' % advertise_afi.upper() + url = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '/%s=%s/%s/route-advertise-list=%s' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_route_advertise_path, advertise_afi_safi) + url += '/config/route-map=%s' % route_map + + return ({'path': url, 'method': DELETE}) + + def get_delete_route_advertise_requests(self, commands, have, is_delete_all): + requests = [] + if not is_delete_all: + for cmd in commands: + vrf_name = cmd['vrf_name'] + addr_fams = cmd.get('address_family', None) + if addr_fams: + addr_fams = addr_fams.get('afis', []) + if not addr_fams: + return requests + for addr_fam in addr_fams: + afi = addr_fam.get('afi', None) + safi = addr_fam.get('safi', None) + route_advertise_list = addr_fam.get('route_advertise_list', []) + if route_advertise_list: + for rt_adv in route_advertise_list: + advertise_afi = rt_adv.get('advertise_afi', None) + route_map = rt_adv.get('route_map', None) + # Check if the commands to be deleted are configured + for conf in have: + conf_vrf_name = conf['vrf_name'] + conf_addr_fams = conf.get('address_family', None) + if conf_addr_fams: + conf_addr_fams = conf_addr_fams.get('afis', []) + for conf_addr_fam in conf_addr_fams: + conf_afi = conf_addr_fam.get('afi', None) + conf_safi = conf_addr_fam.get('safi', None) + conf_route_advertise_list = conf_addr_fam.get('route_advertise_list', []) + if conf_route_advertise_list: + for conf_rt_adv in conf_route_advertise_list: + conf_advertise_afi = conf_rt_adv.get('advertise_afi', None) + conf_route_map = conf_rt_adv.get('route_map', None) + # Deletion at route-advertise level + if (not advertise_afi and vrf_name == conf_vrf_name and afi == conf_afi and safi == conf_safi): + requests.append(self.get_delete_route_advertise_request(vrf_name, afi, safi)) + # Deletion at advertise-afi-safi level + if (advertise_afi and not route_map and vrf_name == conf_vrf_name and afi == conf_afi and safi == + conf_safi and advertise_afi == conf_advertise_afi): + requests.append(self.get_delete_route_advertise_list_request(vrf_name, afi, safi, advertise_afi)) + # Deletion at route-map level + if (route_map and vrf_name == conf_vrf_name and afi == conf_afi and safi == conf_safi + and advertise_afi == conf_advertise_afi and route_map == conf_route_map): + requests.append(self.get_delete_route_advertise_route_map_request(vrf_name, afi, safi, + advertise_afi, route_map)) + + return requests + + def get_delete_dampening_request(self, vrf_name, conf_afi, conf_safi): + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '/%s=%s/route-flap-damping/config/enabled' % (self.afi_safi_path, afi_safi) + + return ({"path": url, "method": DELETE}) + + def get_delete_address_family_request(self, vrf_name, conf_afi, conf_safi): + request = None + + if conf_afi != "l2vpn": + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '/%s=openconfig-bgp-types:%s' % (self.afi_safi_path, afi_safi) + request = {"path": url, "method": DELETE} + + return request + + def get_delete_single_bgp_af_request(self, conf, is_delete_all, match=None): + requests = [] + vrf_name = conf['vrf_name'] + + conf_addr_fams = conf.get('address_family', None) + if conf_addr_fams is None: + return requests + + conf_addr_fams = conf_addr_fams.get('afis', []) + + if match and not conf_addr_fams: + conf_addr_fams = match.get('address_family', None) + if conf_addr_fams: + conf_addr_fams = conf_addr_fams.get('afis', []) + conf_addr_fams = [{'afi': af['afi'], 'safi': af['safi']} for af in conf_addr_fams] + + if not conf_addr_fams: + return requests + + for conf_addr_fam in conf_addr_fams: + conf_afi = conf_addr_fam.get('afi', None) + conf_safi = conf_addr_fam.get('safi', None) + if not conf_afi or not conf_safi: + continue + conf_redis_arr = conf_addr_fam.get('redistribute', []) + conf_adv_pip = conf_addr_fam.get('advertise_pip', None) + conf_adv_pip_ip = conf_addr_fam.get('advertise_pip_ip', None) + conf_adv_pip_peer_ip = conf_addr_fam.get('advertise_pip_peer_ip', None) + conf_adv_svi_ip = conf_addr_fam.get('advertise_svi_ip', None) + conf_adv_all_vni = conf_addr_fam.get('advertise_all_vni', None) + conf_adv_default_gw = conf_addr_fam.get('advertise_default_gw', None) + conf_max_path = conf_addr_fam.get('max_path', None) + conf_dampening = conf_addr_fam.get('dampening', None) + conf_network = conf_addr_fam.get('network', []) + if is_delete_all: + if conf_adv_pip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip')) + if conf_adv_pip_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip-ip')) + if conf_adv_pip_peer_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip-peer-ip')) + if conf_adv_svi_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-svi-ip')) + if conf_adv_all_vni: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-all-vni')) + if conf_dampening: + requests.append(self.get_delete_dampening_request(vrf_name, conf_afi, conf_safi)) + if conf_network: + requests.extend(self.get_delete_network_request(vrf_name, conf_afi, conf_safi, conf_network, is_delete_all, None)) + if conf_adv_default_gw: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-default-gw')) + if conf_redis_arr: + requests.extend(self.get_delete_redistribute_requests(vrf_name, conf_afi, conf_safi, conf_redis_arr, is_delete_all, None)) + if conf_max_path: + requests.extend(self.get_delete_max_path_requests(vrf_name, conf_afi, conf_safi, conf_max_path, is_delete_all, None)) + addr_family_del_req = self.get_delete_address_family_request(vrf_name, conf_afi, conf_safi) + if addr_family_del_req: + requests.append(addr_family_del_req) + elif match: + match_addr_fams = match.get('address_family', None) + if match_addr_fams: + match_addr_fams = match_addr_fams.get('afis', []) + if not match_addr_fams: + continue + for match_addr_fam in match_addr_fams: + mat_afi = match_addr_fam.get('afi', None) + mat_safi = match_addr_fam.get('safi', None) + if mat_afi and mat_safi and mat_afi == conf_afi and mat_safi == conf_safi: + mat_advt_pip = match_addr_fam.get('advertise_pip', None) + mat_advt_pip_ip = match_addr_fam.get('advertise_pip_ip', None) + mat_advt_pip_peer_ip = match_addr_fam.get('advertise_pip_peer_ip', None) + mat_advt_svi_ip = match_addr_fam.get('advertise_svi_ip', None) + mat_advt_all_vni = match_addr_fam.get('advertise_all_vni', None) + mat_redis_arr = match_addr_fam.get('redistribute', []) + mat_advt_defaut_gw = match_addr_fam.get('advertise_default_gw', None) + mat_max_path = match_addr_fam.get('max_path', None) + mat_dampening = match_addr_fam.get('dampening', None) + mat_network = match_addr_fam.get('network', []) + + if (conf_adv_pip is None and conf_adv_pip_ip is None and conf_adv_pip_peer_ip is None and conf_adv_svi_ip is None + and conf_adv_all_vni is None and not conf_redis_arr and conf_adv_default_gw is None + and not conf_max_path and conf_dampening is None and not conf_network): + if mat_advt_pip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip')) + if mat_advt_pip_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip-ip')) + if mat_advt_pip_peer_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip-peer-ip')) + if mat_advt_svi_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-svi-ip')) + if mat_advt_all_vni is not None: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-all-vni')) + if mat_dampening is not None: + requests.append(self.get_delete_dampening_request(vrf_name, conf_afi, conf_safi)) + if mat_advt_defaut_gw: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-default-gw')) + if mat_redis_arr: + requests.extend(self.get_delete_redistribute_requests(vrf_name, conf_afi, conf_safi, mat_redis_arr, False, mat_redis_arr)) + if mat_max_path: + requests.extend(self.get_delete_max_path_requests(vrf_name, conf_afi, conf_safi, mat_max_path, is_delete_all, mat_max_path)) + if mat_network: + requests.extend(self.get_delete_network_request(vrf_name, conf_afi, conf_safi, mat_network, False, mat_network)) + addr_family_del_req = self.get_delete_address_family_request(vrf_name, conf_afi, conf_safi) + if addr_family_del_req: + requests.append(addr_family_del_req) + else: + if conf_adv_pip and mat_advt_pip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip')) + if conf_adv_pip_ip and mat_advt_pip_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip-ip')) + if conf_adv_pip_peer_ip and mat_advt_pip_peer_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip-peer-ip')) + if conf_adv_svi_ip and mat_advt_svi_ip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-svi-ip')) + if conf_adv_all_vni and mat_advt_all_vni: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-all-vni')) + if conf_dampening and mat_dampening: + requests.append(self.get_delete_dampening_request(vrf_name, conf_afi, conf_safi)) + if conf_adv_default_gw and mat_advt_defaut_gw: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-default-gw')) + if conf_redis_arr and mat_redis_arr: + requests.extend(self.get_delete_redistribute_requests(vrf_name, conf_afi, conf_safi, conf_redis_arr, False, mat_redis_arr)) + if conf_max_path and mat_max_path: + requests.extend(self.get_delete_max_path_requests(vrf_name, conf_afi, conf_safi, conf_max_path, is_delete_all, mat_max_path)) + if conf_network and mat_network: + requests.extend(self.get_delete_network_request(vrf_name, conf_afi, conf_safi, conf_network, False, mat_network)) + break + + return requests + + def get_delete_network_request(self, vrf_name, conf_afi, conf_safi, conf_network, is_delete_all, mat_network): + requests = [] + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '%s=%s/network-config/network=' % (self.afi_safi_path, afi_safi) + mat_list = [] + for conf in conf_network: + if mat_network: + mat_prefix = next((pre for pre in mat_network if pre == conf), None) + if mat_prefix: + mat_list.append(mat_prefix) + if not is_delete_all and mat_list: + for each in mat_list: + tmp = each.replace('/', '%2f') + requests.append({'path': url + tmp, 'method': DELETE}) + elif is_delete_all: + for each in conf_network: + tmp = each.replace('/', '%2f') + requests.append({'path': url + tmp, 'method': DELETE}) + return requests + + def get_delete_max_path_requests(self, vrf_name, conf_afi, conf_safi, conf_max_path, is_delete_all, mat_max_path): + requests = [] + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + url += '%s=%s/use-multiple-paths/' % (self.afi_safi_path, afi_safi) + + conf_ebgp = conf_max_path.get('ebgp', None) + conf_ibgp = conf_max_path.get('ibgp', None) + mat_ebgp = None + mat_ibgp = None + if mat_max_path: + mat_ebgp = mat_max_path.get('ebgp', None) + mat_ibgp = mat_max_path.get('ibgp', None) + + if (conf_ebgp and mat_ebgp) or is_delete_all: + requests.append({'path': url + 'ebgp', 'method': DELETE}) + if (conf_ibgp and mat_ibgp) or is_delete_all: + requests.append({'path': url + 'ibgp', 'method': DELETE}) + + return requests + + def get_delete_route_map_request(self, vrf_name, conf_afi, conf_redis, conf_route_map): + addr_family = "openconfig-types:%s" % (conf_afi.upper()) + conf_protocol = conf_redis['protocol'].upper() + if conf_protocol == 'CONNECTED': + conf_protocol = "DIRECTLY_CONNECTED" + src_protocol = "openconfig-policy-types:%s" % (conf_protocol) + dst_protocol = "openconfig-policy-types:BGP" + url = '%s=%s/%s=' % (self.network_instance_path, vrf_name, self.table_connection_path) + url += '%s,%s,%s/config/import-policy=%s' % (src_protocol, dst_protocol, addr_family, conf_route_map) + return ({'path': url, 'method': DELETE}) + + def get_delete_redistribute_requests(self, vrf_name, conf_afi, conf_safi, conf_redis_arr, is_delete_all, mat_redis_arr): + requests = [] + for conf_redis in conf_redis_arr: + addr_family = "openconfig-types:%s" % (conf_afi.upper()) + conf_protocol = conf_redis['protocol'].upper() + + ext_metric_flag = False + ext_route_flag = False + mat_redis = None + mat_metric = None + mat_route_map = None + if not is_delete_all: + mat_redis = next((redis_cfg for redis_cfg in mat_redis_arr if redis_cfg['protocol'].upper() == conf_protocol), None) + if mat_redis: + mat_metric = mat_redis.get('metric', None) + mat_route_map = mat_redis.get('route_map', None) + if mat_metric: + ext_metric_flag = True + if mat_route_map: + ext_route_flag = True + + if conf_protocol == 'CONNECTED': + conf_protocol = "DIRECTLY_CONNECTED" + + src_protocol = "openconfig-policy-types:%s" % (conf_protocol) + dst_protocol = "openconfig-policy-types:BGP" + + conf_route_map = conf_redis.get('route_map', None) + conf_metric = conf_redis.get('metric', None) + if conf_metric is not None: + conf_metric = float(conf_redis['metric']) + + url = '%s=%s/%s=' % (self.network_instance_path, vrf_name, self.table_connection_path) + + new_metric_flag = conf_metric is not None + new_route_flag = conf_route_map is not None + is_delete_protocol = False + if is_delete_all: + is_delete_protocol = True + else: + is_delete_protocol = (new_metric_flag == ext_metric_flag) and (new_route_flag == ext_route_flag) + + if is_delete_protocol: + url += '%s,%s,%s' % (src_protocol, dst_protocol, addr_family) + requests.append({'path': url, 'method': DELETE}) + continue + + if new_metric_flag and ext_metric_flag: + url += '%s,%s,%s/config/metric' % (src_protocol, dst_protocol, addr_family) + requests.append({'path': url, 'method': DELETE}) + + if new_route_flag and ext_route_flag: + url += '%s,%s,%s/config/import-policy=%s' % (src_protocol, dst_protocol, addr_family, conf_route_map) + requests.append({'path': url, 'method': DELETE}) + + return requests + + def get_delete_bgp_af_requests(self, commands, have, is_delete_all): + requests = [] + for cmd in commands: + vrf_name = cmd['vrf_name'] + as_val = cmd['bgp_as'] + match_cfg = None + if not is_delete_all: + match_cfg = next((have_cfg for have_cfg in have if have_cfg['vrf_name'] == vrf_name and have_cfg['bgp_as'] == as_val), None) + requests.extend(self.get_delete_single_bgp_af_request(cmd, is_delete_all, match_cfg)) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_as_paths/bgp_as_paths.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_as_paths/bgp_as_paths.py new file mode 100644 index 000000000..dc2b023b1 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_as_paths/bgp_as_paths.py @@ -0,0 +1,304 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_as_paths 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 ( + update_states, + get_diff, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import to_request +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + +try: + from urllib.parse import urlencode +except Exception: + from urllib import urlencode + + +class Bgp_as_paths(ConfigBase): + """ + The sonic_bgp_as_paths class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bgp_as_paths', + ] + + def __init__(self, module): + super(Bgp_as_paths, self).__init__(module) + + def get_bgp_as_paths_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) + bgp_as_paths_facts = facts['ansible_network_resources'].get('bgp_as_paths') + if not bgp_as_paths_facts: + return [] + return bgp_as_paths_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_bgp_as_paths_facts = self.get_bgp_as_paths_facts() + commands, requests = self.set_config(existing_bgp_as_paths_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_bgp_as_paths_facts = self.get_bgp_as_paths_facts() + + result['before'] = existing_bgp_as_paths_facts + if result['changed']: + result['after'] = changed_bgp_as_paths_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bgp_as_paths_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_bgp_as_paths_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + for i in want: + if i.get('members'): + temp = [] + for j in i['members']: + temp.append(j.replace('\\\\', '\\')) + i['members'] = temp + diff = get_diff(want, have) + for i in want: + if i.get('members'): + temp = [] + for j in i['members']: + temp.append(j.replace('\\', '\\\\')) + i['members'] = temp + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + @staticmethod + def _state_replaced(**kwargs): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + return commands + + @staticmethod + def _state_overridden(**kwargs): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + return commands + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_as_path_list_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # To Delete a single member + # data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets/as-path-set=xyz/config/as-path-set-member=11 + # This will delete the as path and its all members + # data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets/as-path-set=xyz + # This will delete ALL as path completely + # data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets + + is_delete_all = False + # if want is none, then delete ALL + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests = self.get_delete_as_path_requests(commands, have, is_delete_all) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_new_add_request(self, conf): + request = None + members = conf.get('members', None) + permit = conf.get('permit', None) + permit_str = "" + if permit: + permit_str = "PERMIT" + else: + permit_str = "DENY" + if members: + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets" + method = "PATCH" + cfg = {'as-path-set-name': conf['name'], 'as-path-set-member': members, 'openconfig-bgp-policy-ext:action': permit_str} + as_path_set = {'as-path-set-name': conf['name'], 'config': cfg} + payload = {'openconfig-bgp-policy:as-path-sets': {'as-path-set': [as_path_set]}} + request = {"path": url, "method": method, "data": payload} + return request + + def get_delete_all_as_path_requests(self, commands): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets" + method = "DELETE" + requests = [] + if commands: + request = {"path": url, "method": method} + requests.append(request) + return requests + + def get_delete_single_as_path_member_requests(self, name, members): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:" + url = url + "bgp-defined-sets/as-path-sets/as-path-set={name}/config/{members_param}" + method = "DELETE" + members_params = {'as-path-set-member': ','.join(members)} + members_str = urlencode(members_params) + request = {"path": url.format(name=name, members_param=members_str), "method": method} + return request + + def get_delete_single_as_path_requests(self, name): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets/as-path-set={}" + method = "DELETE" + request = {"path": url.format(name), "method": method} + return request + + def get_delete_single_as_path_action_requests(self, name): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets/as-path-set={}" + url = url + "/openconfig-bgp-policy-ext:action" + method = "DELETE" + request = {"path": url.format(name), "method": method} + return request + + def get_delete_as_path_requests(self, commands, have, is_delete_all): + requests = [] + if is_delete_all: + requests = self.get_delete_all_as_path_requests(commands) + else: + for cmd in commands: + name = cmd['name'] + members = cmd['members'] + permit = cmd['permit'] + if members: + diff_members = [] + for item in have: + if item['name'] == name: + for member_want in cmd['members']: + if item['members']: + if str(member_want) in item['members']: + diff_members.append(member_want) + if diff_members: + requests.append(self.get_delete_single_as_path_member_requests(name, diff_members)) + + elif permit: + for item in have: + if item['name'] == name: + requests.append(self.get_delete_single_as_path_action_requests(name)) + else: + for item in have: + if item['name'] == name: + requests.append(self.get_delete_single_as_path_requests(name)) + + return requests + + def get_modify_as_path_list_requests(self, commands, have): + requests = [] + if not commands: + return requests + + for conf in commands: + new_req = self.get_new_add_request(conf) + if new_req: + requests.append(new_req) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_communities/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_communities/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_communities/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_communities/bgp_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_communities/bgp_communities.py new file mode 100644 index 000000000..670fb26d3 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_communities/bgp_communities.py @@ -0,0 +1,368 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_communities 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 ( + update_states, + get_diff, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import to_request +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError +import json +from ansible.module_utils._text import to_native +import traceback +try: + import jinja2 + HAS_LIB = True +except Exception as e: + HAS_LIB = False + ERR_MSG = to_native(e) + LIB_IMP_ERR = traceback.format_exc() + +try: + from urllib.parse import urlencode +except Exception: + from urllib import urlencode + + +class Bgp_communities(ConfigBase): + """ + The sonic_bgp_communities class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bgp_communities', + ] + + def __init__(self, module): + super(Bgp_communities, self).__init__(module) + + def get_bgp_communities_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) + bgp_communities_facts = facts['ansible_network_resources'].get('bgp_communities') + if not bgp_communities_facts: + return [] + return bgp_communities_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_bgp_communities_facts = self.get_bgp_communities_facts() + commands, requests = self.set_config(existing_bgp_communities_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_bgp_communities_facts = self.get_bgp_communities_facts() + + result['before'] = existing_bgp_communities_facts + if result['changed']: + result['after'] = changed_bgp_communities_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bgp_communities_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_bgp_communities_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + diff = get_diff(want, have) + # with open('/root/ansible_log.log', 'a+') as fp: + # fp.write('comm: want: ' + str(want) + '\n') + # fp.write('comm: have: ' + str(have) + '\n') + # fp.write('comm: diff: ' + str(diff) + '\n') + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + @staticmethod + def _state_replaced(**kwargs): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + return commands + + @staticmethod + def _state_overridden(**kwargs): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + return commands + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_bgp_community_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # Delete a community + # https://100.94.81.19/restconf/data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets/community-set=extest + # Delete all members but not community + # https://100.94.81.19/restconf/data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets/community-set=extest/config/community-member + # Dete a memeber from the expanded community + # https://100.94.81.19/restconf/data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets/community-set=extest/config/community-member=REGEX%3A100.100 + # Delete ALL Bgp_communities and its members + # https://100.94.81.19/restconf/data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets + is_delete_all = False + # if want is none, then delete ALL + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests = self.get_delete_bgp_communities(commands, have, is_delete_all) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_delete_single_bgp_community_member_requests(self, name, type, members): + requests = [] + for member in members: + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:" + url = url + "bgp-defined-sets/community-sets/community-set={name}/config/{members_param}" + method = "DELETE" + memberstr = member + if type == 'expanded': + memberstr = 'REGEX:' + member + members_params = {'community-member': memberstr} + members_str = urlencode(members_params) + request = {"path": url.format(name=name, members_param=members_str), "method": method} + requests.append(request) + return requests + + def get_delete_all_members_bgp_community_requests(self, name): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:" + url = url + "bgp-defined-sets/community-sets/community-set={}/config/community-member" + method = "DELETE" + request = {"path": url.format(name), "method": method} + return request + + def get_delete_single_bgp_community_requests(self, name): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets/community-set={}" + method = "DELETE" + request = {"path": url.format(name), "method": method} + return request + + def get_delete_all_bgp_communities(self, commands): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets" + method = "DELETE" + requests = [] + if commands: + request = {"path": url, "method": method} + requests.append(request) + return requests + + def get_delete_bgp_communities(self, commands, have, is_delete_all): + # with open('/root/ansible_log.log', 'a+') as fp: + # fp.write('bgp_commmunities: delete requests ************** \n') + requests = [] + if is_delete_all: + requests = self.get_delete_all_bgp_communities(commands) + else: + for cmd in commands: + name = cmd['name'] + type = cmd['type'] + members = cmd['members'] + if members: + if members['regex']: + diff_members = [] + for item in have: + if item['name'] == name and item['members']: + for member_want in members['regex']: + if str(member_want) in item['members']['regex']: + diff_members.append(member_want) + if diff_members: + requests.extend(self.get_delete_single_bgp_community_member_requests(name, type, diff_members)) + else: + for item in have: + if item['name'] == name: + if item['members']: + requests.append(self.get_delete_all_members_bgp_community_requests(name)) + else: + for item in have: + if item['name'] == name: + requests.append(self.get_delete_single_bgp_community_requests(name)) + + # with open('/root/ansible_log.log', 'a+') as fp: + # fp.write('bgp_commmunities: delete requests' + str(requests) + '\n') + return requests + + def get_new_add_request(self, conf): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets" + method = "PATCH" + # members = conf['members'] + # members_str = ', '.join(members) + # members_list = list() + # for member in members.split(','): + # members_list.append(str(member)) + + if 'match' not in conf: + conf['match'] = "ANY" + # with open('/root/ansible_log.log', 'a+') as fp: + # fp.write('bgp_communities: conf' + str(conf) + '\n') + if 'local_as' in conf and conf['local_as']: + conf['members']['regex'].append("NO_EXPORT_SUBCONFED") + if 'no_peer' in conf and conf['no_peer']: + conf['members']['regex'].append("NOPEER") + if 'no_export' in conf and conf['no_export']: + conf['members']['regex'].append("NO_EXPORT") + if 'no_advertise' in conf and conf['no_advertise']: + conf['members']['regex'].append("NO_ADVERTISE") + input_data = {'name': conf['name'], 'members_list': conf['members']['regex'], 'match': conf['match']} + if conf['type'] == 'expanded': + input_data['regex'] = "REGEX:" + else: + input_data['regex'] = "" + if conf['permit']: + input_data['permit'] = "PERMIT" + else: + input_data['permit'] = "DENY" + payload_template = """ + { + "openconfig-bgp-policy:community-sets": { + "community-set": [ + { + "community-set-name": "{{name}}", + "config": { + "community-set-name": "{{name}}", + "community-member": [ + {% for member in members_list %}"{{regex}}{{member}}"{%- if not loop.last -%},{% endif %}{%endfor%} + ], + "openconfig-bgp-policy-ext:action": "{{permit}}", + "match-set-options": "{{match}}" + } + } + ] + } + }""" + env = jinja2.Environment(autoescape=False) + t = env.from_string(payload_template) + intended_payload = t.render(input_data) + ret_payload = json.loads(intended_payload) + request = {"path": url, "method": method, "data": ret_payload} + # with open('/root/ansible_log.log', 'a+') as fp: + # fp.write('bgp_communities: request' + str(request) + '\n') + return request + + def get_modify_bgp_community_requests(self, commands, have): + requests = [] + if not commands: + return requests + + for conf in commands: + for item in have: + if item['name'] == conf['name']: + if 'type' not in conf: + conf['type'] = item['type'] + if 'permit' not in conf: + conf['permit'] = item['permit'] + if 'match' not in conf: + conf['match'] = item['match'] + if 'members' not in conf: + conf['members'] = item['members'] + new_req = self.get_new_add_request(conf) + if new_req: + requests.append(new_req) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_ext_communities/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_ext_communities/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_ext_communities/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_ext_communities/bgp_ext_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_ext_communities/bgp_ext_communities.py new file mode 100644 index 000000000..751f88e48 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_ext_communities/bgp_ext_communities.py @@ -0,0 +1,371 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_ext_communities 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 ( + update_states, + get_diff, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import to_request +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +import json +from ansible.module_utils._text import to_native +from ansible.module_utils.connection import ConnectionError +import traceback +try: + import jinja2 + HAS_LIB = True +except Exception as e: + HAS_LIB = False + ERR_MSG = to_native(e) + LIB_IMP_ERR = traceback.format_exc() + +try: + from urllib.parse import urlencode +except Exception: + from urllib import urlencode + + +class Bgp_ext_communities(ConfigBase): + """ + The sonic_bgp_ext_communities class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bgp_ext_communities', + ] + + def __init__(self, module): + super(Bgp_ext_communities, self).__init__(module) + + def get_bgp_ext_communities_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) + bgp_ext_communities_facts = facts['ansible_network_resources'].get('bgp_ext_communities') + if not bgp_ext_communities_facts: + return [] + return bgp_ext_communities_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_bgp_ext_communities_facts = self.get_bgp_ext_communities_facts() + commands, requests = self.set_config(existing_bgp_ext_communities_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_bgp_ext_communities_facts = self.get_bgp_ext_communities_facts() + + result['before'] = existing_bgp_ext_communities_facts + if result['changed']: + result['after'] = changed_bgp_ext_communities_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bgp_ext_communities_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_bgp_ext_communities_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + new_want = self.validate_type(want) + diff = get_diff(new_want, have) + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + @staticmethod + def _state_replaced(**kwargs): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + return commands + + @staticmethod + def _state_overridden(**kwargs): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + return commands + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_bgp_ext_community_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + is_delete_all = False + # if want is none, then delete ALL + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests = self.get_delete_bgp_ext_communities(commands, have, is_delete_all) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_delete_single_bgp_ext_community_member_requests(self, name, type, members): + requests = [] + for member in members: + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:" + url = url + "bgp-defined-sets/ext-community-sets/ext-community-set={name}/config/{members_param}" + method = "DELETE" + members_params = {'ext-community-member': member} + members_str = urlencode(members_params) + request = {"path": url.format(name=name, members_param=members_str), "method": method} + requests.append(request) + return requests + + def get_delete_all_members_bgp_ext_community_requests(self, name): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:" + url = url + "bgp-defined-sets/ext-community-sets/ext-community-set={}/config/ext-community-member" + method = "DELETE" + request = {"path": url.format(name), "method": method} + return request + + def get_delete_single_bgp_ext_community_requests(self, name): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/ext-community-sets/ext-community-set={}" + method = "DELETE" + request = {"path": url.format(name), "method": method} + return request + + def get_delete_all_bgp_ext_communities(self, commands): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/ext-community-sets" + method = "DELETE" + requests = [] + if commands: + request = {"path": url, "method": method} + requests.append(request) + return requests + + def get_delete_bgp_ext_communities(self, commands, have, is_delete_all): + requests = [] + if is_delete_all: + requests = self.get_delete_all_bgp_ext_communities(commands) + else: + for cmd in commands: + name = cmd['name'] + type = cmd['type'] + members = cmd['members'] + if members: + if members['regex'] or members['route_origin'] or members['route_target']: + diff_members = [] + for item in have: + if item['name'] == name and item['members']: + if members['regex']: + for member_want in members['regex']: + if str(member_want) in item['members']['regex']: + diff_members.append('REGEX:' + str(member_want)) + if members['route_origin']: + for member_want in members['route_origin']: + if str(member_want) in item['members']['route_origin']: + diff_members.append("route-origin:" + str(member_want)) + if members['route_target']: + for member_want in members['route_target']: + if str(member_want) in item['members']['route_target']: + diff_members.append("route-target:" + str(member_want)) + if diff_members: + requests.extend(self.get_delete_single_bgp_ext_community_member_requests(name, type, diff_members)) + else: + for item in have: + if item['name'] == name: + if item['members']: + requests.append(self.get_delete_all_members_bgp_ext_community_requests(name)) + else: + for item in have: + if item['name'] == name: + requests.append(self.get_delete_single_bgp_ext_community_requests(name)) + + return requests + + def get_new_add_request(self, conf): + + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/ext-community-sets" + method = "PATCH" + members = conf.get('members', None) + if 'match' not in conf: + conf['match'] = "ANY" + else: + conf['match'] = conf['match'].upper() + input_data = {'name': conf['name'], 'match': conf['match']} + + input_data['members_list'] = list() + if members: + regex = members.get('regex', None) + if regex: + input_data['members_list'].extend(["REGEX:" + cfg for cfg in regex]) + else: + route_target = members.get('route_target', None) + if route_target: + input_data['members_list'].extend(["route-target:" + cfg for cfg in route_target]) + route_origin = members.get('route_origin', None) + if route_origin: + input_data['members_list'].extend(["route-origin:" + cfg for cfg in route_origin]) + + if conf['type'] == 'expanded': + input_data['regex'] = "REGEX:" + else: + input_data['regex'] = "" + if conf['permit']: + input_data['permit'] = "PERMIT" + else: + input_data['permit'] = "DENY" + payload_template = """ + { + "openconfig-bgp-policy:ext-community-sets": { + "ext-community-set": [ + { + "ext-community-set-name": "{{name}}", + "config": { + "ext-community-set-name": "{{name}}", + "ext-community-member": [ + {% for member in members_list %}"{{member}}"{%- if not loop.last -%},{% endif %}{%endfor%} + ], + "openconfig-bgp-policy-ext:action": "{{permit}}", + "match-set-options": "{{match}}" + } + } + ] + } + }""" + env = jinja2.Environment(autoescape=False) + t = env.from_string(payload_template) + intended_payload = t.render(input_data) + ret_payload = json.loads(intended_payload) + request = {"path": url, "method": method, "data": ret_payload} + return request + + def get_modify_bgp_ext_community_requests(self, commands, have): + requests = [] + if not commands: + return requests + + for conf in commands: + for item in have: + if item['name'] == conf['name']: + if 'type' not in conf: + conf['type'] = item['type'] + if 'permit' not in conf: + conf['permit'] = item['permit'] + if 'match' not in conf: + conf['match'] = item['match'] + if 'members' not in conf: + conf['members'] = item['members'] + break + new_req = self.get_new_add_request(conf) + if new_req: + requests.append(new_req) + return requests + + def validate_type(self, want): + new_want = [] + if want: + for conf in want: + cfg = conf.copy() + cfg['type'] = 'standard' + members = conf.get('members', None) + if members and members.get('regex', None): + cfg['type'] = 'expanded' + + new_want.append(cfg) + return new_want diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_neighbors/bgp_neighbors.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_neighbors/bgp_neighbors.py new file mode 100644 index 000000000..31bbec78d --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_neighbors/bgp_neighbors.py @@ -0,0 +1,1100 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_bgp_neighbors 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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( + validate_bgps, + normalize_neighbors_interface_name, + get_ip_afi_cfg_payload, + get_prefix_limit_payload +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import to_request +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'delete' + +TEST_KEYS = [ + {'config': {'vrf_name': '', 'bgp_as': ''}}, + {'neighbors': {'neighbor': ''}}, + {'peer_group': {'name': ''}}, + {'afis': {'afi': '', 'safi': ''}}, +] + + +class Bgp_neighbors(ConfigBase): + """ + The sonic_bgp_neighbors class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bgp_neighbors', + ] + + network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' + protocol_bgp_path = 'protocols/protocol=BGP,bgp/bgp' + neighbor_path = 'neighbors/neighbor' + + def __init__(self, module): + super(Bgp_neighbors, self).__init__(module) + + def get_bgp_neighbors_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) + bgp_facts = facts['ansible_network_resources'].get('bgp_neighbors') + if not bgp_facts: + bgp_facts = [] + return bgp_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_bgp_facts = self.get_bgp_neighbors_facts() + commands, requests = self.set_config(existing_bgp_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_bgp_facts = self.get_bgp_neighbors_facts() + + result['before'] = existing_bgp_facts + if result['changed']: + result['after'] = changed_bgp_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bgp_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'] + normalize_neighbors_interface_name(want, self._module) + have = existing_bgp_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + requests = [] + commands = diff + validate_bgps(self._module, commands, have) + requests = self.get_modify_bgp_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + is_delete_all = False + if not want: + is_delete_all = True + if is_delete_all: + commands = have + new_have = have + else: + new_have = self.remove_default_entries(have) + d_diff = get_diff(want, new_have, TEST_KEYS, is_skeleton=True) + delete_diff = get_diff(want, d_diff, TEST_KEYS, is_skeleton=True) + commands = delete_diff + requests = self.get_delete_bgp_neighbor_requests(commands, new_have, want, is_delete_all) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + return commands, requests + + def remove_default_entries(self, data): + new_data = [] + if not data: + return new_data + for conf in data: + new_conf = {} + as_val = conf['bgp_as'] + vrf_name = conf['vrf_name'] + new_conf['bgp_as'] = as_val + new_conf['vrf_name'] = vrf_name + peergroup = conf.get('peer_group', None) + new_peergroups = [] + if peergroup is not None: + for pg in peergroup: + new_pg = {} + pg_val = pg.get('name', None) + new_pg['name'] = pg_val + remote_as = pg.get('remote_as', None) + new_remote = {} + if remote_as: + peer_as = remote_as.get('peer_as', None) + peer_type = remote_as.get('peer_type', None) + if peer_as is not None: + new_remote['peer_as'] = peer_as + if peer_type is not None: + new_remote['peer_type'] = peer_type + if new_remote: + new_pg['remote_as'] = new_remote + timers = pg.get('timers', None) + new_timers = {} + if timers: + keepalive = timers.get('keepalive', None) + holdtime = timers.get('holdtime', None) + connect_retry = timers.get('connect_retry', None) + if keepalive is not None and keepalive != 60: + new_timers['keepalive'] = keepalive + if holdtime is not None and holdtime != 180: + new_timers['holdtime'] = holdtime + if connect_retry is not None and connect_retry != 30: + new_timers['connect_retry'] = connect_retry + if new_timers: + new_pg['timers'] = new_timers + advertisement_interval = pg.get('advertisement_interval', None) + if advertisement_interval is not None and advertisement_interval != 30: + new_pg['advertisement_interval'] = advertisement_interval + bfd = pg.get('bfd', None) + if bfd is not None: + new_pg['bfd'] = bfd + capability = pg.get('capability', None) + if capability is not None: + new_pg['capability'] = capability + afi = [] + address_family = pg.get('address_family', None) + if address_family: + if address_family.get('afis', None): + for each in address_family['afis']: + if each: + tmp = {} + if each.get('afi', None) is not None: + tmp['afi'] = each['afi'] + if each.get('safi', None) is not None: + tmp['safi'] = each['safi'] + if each.get('activate', None) is not None and each['activate'] is not False: + tmp['activate'] = each['activate'] + if each.get('allowas_in', None) is not None: + tmp['allowas_in'] = each['allowas_in'] + if each.get('ip_afi', None) is not None: + tmp['ip_afi'] = each['ip_afi'] + if each.get('prefix_limit', None) is not None: + tmp['prefix_limit'] = each['prefix_limit'] + if each.get('prefix_list_in', None) is not None: + tmp['prefix_list_in'] = each['prefix_list_in'] + if each.get('prefix_list_out', None) is not None: + tmp['prefix_list_out'] = each['prefix_list_out'] + afi.append(tmp) + if afi and len(afi) > 0: + afis = {} + afis.update({'afis': afi}) + new_pg['address_family'] = afis + if new_pg: + new_peergroups.append(new_pg) + if new_peergroups: + new_conf['peer_group'] = new_peergroups + neighbors = conf.get('neighbors', None) + new_neighbors = [] + if neighbors is not None: + for neighbor in neighbors: + new_neighbor = {} + neighbor_val = neighbor.get('neighbor', None) + new_neighbor['neighbor'] = neighbor_val + remote_as = neighbor.get('remote_as', None) + new_remote = {} + if remote_as: + peer_as = remote_as.get('peer_as', None) + peer_type = remote_as.get('peer_type', None) + if peer_as is not None: + new_remote['peer_as'] = peer_as + if peer_type is not None: + new_remote['peer_type'] = peer_type + if new_remote: + new_neighbor['remote_as'] = new_remote + peer_group = neighbor.get('peer_group', None) + if peer_group: + new_neighbor['peer_group'] = peer_group + timers = neighbor.get('timers', None) + new_timers = {} + if timers: + keepalive = timers.get('keepalive', None) + holdtime = timers.get('holdtime', None) + connect_retry = timers.get('connect_retry', None) + if keepalive is not None and keepalive != 60: + new_timers['keepalive'] = keepalive + if holdtime is not None and holdtime != 180: + new_timers['holdtime'] = holdtime + if connect_retry is not None and connect_retry != 30: + new_timers['connect_retry'] = connect_retry + if new_timers: + new_neighbor['timers'] = new_timers + advertisement_interval = neighbor.get('advertisement_interval', None) + if advertisement_interval is not None and advertisement_interval != 30: + new_neighbor['advertisement_interval'] = advertisement_interval + bfd = neighbor.get('bfd', None) + if bfd is not None: + new_neighbor['bfd'] = bfd + capability = neighbor.get('capability', None) + if capability is not None: + new_neighbor['capability'] = capability + if new_neighbor: + new_neighbors.append(new_neighbor) + if new_neighbors: + new_conf['neighbors'] = new_neighbors + if new_conf: + new_data.append(new_conf) + return new_data + + def build_bgp_peer_groups_payload(self, cmd, have, bgp_as, vrf_name): + requests = [] + bgp_peer_group_list = [] + for peer_group in cmd: + if peer_group: + bgp_peer_group = {} + peer_group_cfg = {} + tmp_bfd = {} + tmp_ebgp = {} + tmp_timers = {} + tmp_capability = {} + tmp_remote = {} + tmp_transport = {} + afi = [] + if peer_group.get('name', None) is not None: + peer_group_cfg.update({'peer-group-name': peer_group['name']}) + bgp_peer_group.update({'peer-group-name': peer_group['name']}) + if peer_group.get('bfd', None) is not None: + if peer_group['bfd'].get('enabled', None) is not None: + tmp_bfd.update({'enabled': peer_group['bfd']['enabled']}) + if peer_group['bfd'].get('check_failure', None) is not None: + tmp_bfd.update({'check-control-plane-failure': peer_group['bfd']['check_failure']}) + if peer_group['bfd'].get('profile', None) is not None: + tmp_bfd.update({'bfd-profile': peer_group['bfd']['profile']}) + if peer_group.get('auth_pwd', None) is not None: + if (peer_group['auth_pwd'].get('pwd', None) is not None and + peer_group['auth_pwd'].get('encrypted', None) is not None): + bgp_peer_group.update({'auth-password': {'config': {'password': peer_group['auth_pwd']['pwd'], + 'encrypted': peer_group['auth_pwd']['encrypted']}}}) + if peer_group.get('ebgp_multihop', None) is not None: + if peer_group['ebgp_multihop'].get('enabled', None) is not None: + tmp_ebgp.update({'enabled': peer_group['ebgp_multihop']['enabled']}) + if peer_group['ebgp_multihop'].get('multihop_ttl', None) is not None: + tmp_ebgp.update({'multihop-ttl': peer_group['ebgp_multihop']['multihop_ttl']}) + if peer_group.get('timers', None) is not None: + if peer_group['timers'].get('holdtime', None) is not None: + tmp_timers.update({'hold-time': peer_group['timers']['holdtime']}) + if peer_group['timers'].get('keepalive', None) is not None: + tmp_timers.update({'keepalive-interval': peer_group['timers']['keepalive']}) + if peer_group['timers'].get('connect_retry', None) is not None: + tmp_timers.update({'connect-retry': peer_group['timers']['connect_retry']}) + if peer_group.get('capability', None) is not None: + if peer_group['capability'].get('dynamic', None) is not None: + tmp_capability.update({'capability-dynamic': peer_group['capability']['dynamic']}) + if peer_group['capability'].get('extended_nexthop', None) is not None: + tmp_capability.update({'capability-extended-nexthop': peer_group['capability']['extended_nexthop']}) + if peer_group.get('pg_description', None) is not None: + peer_group_cfg.update({'description': peer_group['pg_description']}) + if peer_group.get('disable_connected_check', None) is not None: + peer_group_cfg.update({'disable-ebgp-connected-route-check': peer_group['disable_connected_check']}) + if peer_group.get('dont_negotiate_capability', None) is not None: + peer_group_cfg.update({'dont-negotiate-capability': peer_group['dont_negotiate_capability']}) + if peer_group.get('enforce_first_as', None) is not None: + peer_group_cfg.update({'enforce-first-as': peer_group['enforce_first_as']}) + if peer_group.get('enforce_multihop', None) is not None: + peer_group_cfg.update({'enforce-multihop': peer_group['enforce_multihop']}) + if peer_group.get('override_capability', None) is not None: + peer_group_cfg.update({'override-capability': peer_group['override_capability']}) + if peer_group.get('shutdown_msg', None) is not None: + peer_group_cfg.update({'shutdown-message': peer_group['shutdown_msg']}) + if peer_group.get('solo', None) is not None: + peer_group_cfg.update({'solo-peer': peer_group['solo']}) + if peer_group.get('strict_capability_match', None) is not None: + peer_group_cfg.update({'strict-capability-match': peer_group['strict_capability_match']}) + if peer_group.get('ttl_security', None) is not None: + peer_group_cfg.update({'ttl-security-hops': peer_group['ttl_security']}) + if peer_group.get('local_as', None) is not None: + if peer_group['local_as'].get('as', None) is not None: + peer_group_cfg.update({'local-as': peer_group['local_as']['as']}) + if peer_group['local_as'].get('no_prepend', None) is not None: + peer_group_cfg.update({'local-as-no-prepend': peer_group['local_as']['no_prepend']}) + if peer_group['local_as'].get('replace_as', None) is not None: + peer_group_cfg.update({'local-as-replace-as': peer_group['local_as']['replace_as']}) + if peer_group.get('local_address', None) is not None: + tmp_transport.update({'local-address': peer_group['local_address']}) + if peer_group.get('passive', None) is not None: + tmp_transport.update({'passive-mode': peer_group['passive']}) + if peer_group.get('advertisement_interval', None) is not None: + tmp_timers.update({'minimum-advertisement-interval': peer_group['advertisement_interval']}) + if peer_group.get('remote_as', None) is not None: + have_nei = self.find_pg(have, bgp_as, vrf_name, peer_group) + if peer_group['remote_as'].get('peer_as', None) is not None: + if have_nei: + if have_nei.get("remote_as", None) is not None: + if have_nei["remote_as"].get("peer_type", None) is not None: + del_nei = {} + del_nei.update({'name': have_nei['name']}) + del_nei.update({'remote_as': have_nei['remote_as']}) + requests.extend(self.delete_specific_peergroup_param_request(vrf_name, del_nei)) + tmp_remote.update({'peer-as': peer_group['remote_as']['peer_as']}) + if peer_group['remote_as'].get('peer_type', None) is not None: + if have_nei: + if have_nei.get("remote_as", None) is not None: + if have_nei["remote_as"].get("peer_as", None) is not None: + del_nei = {} + del_nei.update({'name': have_nei['name']}) + del_nei.update({'remote_as': have_nei['remote_as']}) + requests.extend(self.delete_specific_peergroup_param_request(vrf_name, del_nei)) + tmp_remote.update({'peer-type': peer_group['remote_as']['peer_type'].upper()}) + if peer_group.get('address_family', None) is not None: + if peer_group['address_family'].get('afis', None) is not None: + for each in peer_group['address_family']['afis']: + samp = {} + afi_safi_cfg = {} + pfx_lmt_cfg = {} + pfx_lst_cfg = {} + ip_dict = {} + if each.get('afi', None) is not None and each.get('safi', None) is not None: + afi_safi = each['afi'].upper() + "_" + each['safi'].upper() + if afi_safi is not None: + afi_safi_name = 'openconfig-bgp-types:' + afi_safi + if afi_safi_name is not None: + samp.update({'afi-safi-name': afi_safi_name}) + samp.update({'config': {'afi-safi-name': afi_safi_name}}) + if each.get('prefix_limit', None) is not None: + pfx_lmt_cfg = get_prefix_limit_payload(each['prefix_limit']) + if pfx_lmt_cfg and afi_safi == 'L2VPN_EVPN': + samp.update({'l2vpn-evpn': {'prefix-limit': {'config': pfx_lmt_cfg}}}) + else: + if each.get('ip_afi', None) is not None: + afi_safi_cfg = get_ip_afi_cfg_payload(each['ip_afi']) + if afi_safi_cfg: + ip_dict.update({'config': afi_safi_cfg}) + if pfx_lmt_cfg: + ip_dict.update({'prefix-limit': {'config': pfx_lmt_cfg}}) + if ip_dict and afi_safi == 'IPV4_UNICAST': + samp.update({'ipv4-unicast': ip_dict}) + elif ip_dict and afi_safi == 'IPV6_UNICAST': + samp.update({'ipv6-unicast': ip_dict}) + if each.get('activate', None) is not None: + enabled = each['activate'] + if enabled is not None: + samp.update({'config': {'enabled': enabled}}) + if each.get('allowas_in', None) is not None: + have_pg_af = self.find_af(have, bgp_as, vrf_name, peer_group, each['afi'], each['safi']) + if each['allowas_in'].get('origin', None) is not None: + if have_pg_af: + if have_pg_af.get('allowas_in', None) is not None: + if have_pg_af['allowas_in'].get('value', None) is not None: + del_nei = {} + del_nei.update({'name': peer_group['name']}) + afis_list = [] + temp_cfg = {'afi': each['afi'], 'safi': each['safi']} + temp_cfg['allowas_in'] = {'value': have_pg_af['allowas_in']['value']} + afis_list.append(temp_cfg) + del_nei.update({'address_family': {'afis': afis_list}}) + requests.extend(self.delete_specific_peergroup_param_request(vrf_name, del_nei)) + origin = each['allowas_in']['origin'] + samp.update({'allow-own-as': {'config': {'origin': origin, "enabled": bool("true")}}}) + if each['allowas_in'].get('value', None) is not None: + if have_pg_af: + if have_pg_af.get('allowas_in', None) is not None: + if have_pg_af['allowas_in'].get('origin', None) is not None: + del_nei = {} + del_nei.update({'name': peer_group['name']}) + afis_list = [] + temp_cfg = {'afi': each['afi'], 'safi': each['safi']} + temp_cfg['allowas_in'] = {'origin': have_pg_af['allowas_in']['origin']} + afis_list.append(temp_cfg) + del_nei.update({'address_family': {'afis': afis_list}}) + requests.extend(self.delete_specific_peergroup_param_request(vrf_name, del_nei)) + as_count = each['allowas_in']['value'] + samp.update({'allow-own-as': {'config': {'as-count': as_count, "enabled": bool("true")}}}) + if each.get('prefix_list_in', None) is not None: + prefix_list_in = each['prefix_list_in'] + if prefix_list_in is not None: + pfx_lst_cfg.update({'import-policy': prefix_list_in}) + if each.get('prefix_list_out', None) is not None: + prefix_list_out = each['prefix_list_out'] + if prefix_list_out is not None: + pfx_lst_cfg.update({'export-policy': prefix_list_out}) + if pfx_lst_cfg: + samp.update({'prefix-list': {'config': pfx_lst_cfg}}) + if samp: + afi.append(samp) + if tmp_bfd: + bgp_peer_group.update({'enable-bfd': {'config': tmp_bfd}}) + if tmp_ebgp: + bgp_peer_group.update({'ebgp-multihop': {'config': tmp_ebgp}}) + if tmp_timers: + bgp_peer_group.update({'timers': {'config': tmp_timers}}) + if tmp_transport: + bgp_peer_group.update({'transport': {'config': tmp_transport}}) + if afi and len(afi) > 0: + bgp_peer_group.update({'afi-safis': {'afi-safi': afi}}) + if tmp_capability: + peer_group_cfg.update(tmp_capability) + if tmp_remote: + peer_group_cfg.update(tmp_remote) + if peer_group_cfg: + bgp_peer_group.update({'config': peer_group_cfg}) + if bgp_peer_group: + bgp_peer_group_list.append(bgp_peer_group) + payload = {'openconfig-network-instance:peer-groups': {'peer-group': bgp_peer_group_list}} + return payload, requests + + def find_pg(self, have, bgp_as, vrf_name, peergroup): + mat_dict = next((m_peer for m_peer in have if m_peer['bgp_as'] == bgp_as and m_peer['vrf_name'] == vrf_name), None) + if mat_dict and mat_dict.get("peer_group", None) is not None: + mat_pg = next((m for m in mat_dict['peer_group'] if m["name"] == peergroup['name']), None) + return mat_pg + + def find_af(self, have, bgp_as, vrf_name, peergroup, afi, safi): + mat_pg = self.find_pg(have, bgp_as, vrf_name, peergroup) + if mat_pg and mat_pg['address_family'].get('afis', None) is not None: + mat_af = next((af for af in mat_pg['address_family']['afis'] if af['afi'] == afi and af['safi'] == safi), None) + return mat_af + + def find_nei(self, have, bgp_as, vrf_name, neighbor): + mat_dict = next((m_neighbor for m_neighbor in have if m_neighbor['bgp_as'] == bgp_as and m_neighbor['vrf_name'] == vrf_name), None) + if mat_dict and mat_dict.get("neighbors", None) is not None: + mat_neighbor = next((m for m in mat_dict['neighbors'] if m["neighbor"] == neighbor['neighbor']), None) + return mat_neighbor + + def build_bgp_neighbors_payload(self, cmd, have, bgp_as, vrf_name): + bgp_neighbor_list = [] + requests = [] + for neighbor in cmd: + if neighbor: + bgp_neighbor = {} + neighbor_cfg = {} + tmp_bfd = {} + tmp_ebgp = {} + tmp_timers = {} + tmp_capability = {} + tmp_remote = {} + tmp_transport = {} + if neighbor.get('bfd', None) is not None: + if neighbor['bfd'].get('enabled', None) is not None: + tmp_bfd.update({'enabled': neighbor['bfd']['enabled']}) + if neighbor['bfd'].get('check_failure', None) is not None: + tmp_bfd.update({'check-control-plane-failure': neighbor['bfd']['check_failure']}) + if neighbor['bfd'].get('profile', None) is not None: + tmp_bfd.update({'bfd-profile': neighbor['bfd']['profile']}) + if neighbor.get('auth_pwd', None) is not None: + if (neighbor['auth_pwd'].get('pwd', None) is not None and + neighbor['auth_pwd'].get('encrypted', None) is not None): + bgp_neighbor.update({'auth-password': {'config': {'password': neighbor['auth_pwd']['pwd'], + 'encrypted': neighbor['auth_pwd']['encrypted']}}}) + if neighbor.get('ebgp_multihop', None) is not None: + if neighbor['ebgp_multihop'].get('enabled', None) is not None: + tmp_ebgp.update({'enabled': neighbor['ebgp_multihop']['enabled']}) + if neighbor['ebgp_multihop'].get('multihop_ttl', None) is not None: + tmp_ebgp.update({'multihop-ttl': neighbor['ebgp_multihop']['multihop_ttl']}) + if neighbor.get('timers', None) is not None: + if neighbor['timers'].get('holdtime', None) is not None: + tmp_timers.update({'hold-time': neighbor['timers']['holdtime']}) + if neighbor['timers'].get('keepalive', None) is not None: + tmp_timers.update({'keepalive-interval': neighbor['timers']['keepalive']}) + if neighbor['timers'].get('connect_retry', None) is not None: + tmp_timers.update({'connect-retry': neighbor['timers']['connect_retry']}) + if neighbor.get('capability', None) is not None: + if neighbor['capability'].get('dynamic', None) is not None: + tmp_capability.update({'capability-dynamic': neighbor['capability']['dynamic']}) + if neighbor['capability'].get('extended_nexthop', None) is not None: + tmp_capability.update({'capability-extended-nexthop': neighbor['capability']['extended_nexthop']}) + if neighbor.get('advertisement_interval', None) is not None: + tmp_timers.update({'minimum-advertisement-interval': neighbor['advertisement_interval']}) + if neighbor.get('neighbor', None) is not None: + bgp_neighbor.update({'neighbor-address': neighbor['neighbor']}) + neighbor_cfg.update({'neighbor-address': neighbor['neighbor']}) + if neighbor.get('peer_group', None) is not None: + neighbor_cfg.update({'peer-group': neighbor['peer_group']}) + if neighbor.get('nbr_description', None) is not None: + neighbor_cfg.update({'description': neighbor['nbr_description']}) + if neighbor.get('disable_connected_check', None) is not None: + neighbor_cfg.update({'disable-ebgp-connected-route-check': neighbor['disable_connected_check']}) + if neighbor.get('dont_negotiate_capability', None) is not None: + neighbor_cfg.update({'dont-negotiate-capability': neighbor['dont_negotiate_capability']}) + if neighbor.get('enforce_first_as', None) is not None: + neighbor_cfg.update({'enforce-first-as': neighbor['enforce_first_as']}) + if neighbor.get('enforce_multihop', None) is not None: + neighbor_cfg.update({'enforce-multihop': neighbor['enforce_multihop']}) + if neighbor.get('override_capability', None) is not None: + neighbor_cfg.update({'override-capability': neighbor['override_capability']}) + if neighbor.get('port', None) is not None: + neighbor_cfg.update({'peer-port': neighbor['port']}) + if neighbor.get('shutdown_msg', None) is not None: + neighbor_cfg.update({'shutdown-message': neighbor['shutdown_msg']}) + if neighbor.get('solo', None) is not None: + neighbor_cfg.update({'solo-peer': neighbor['solo']}) + if neighbor.get('strict_capability_match', None) is not None: + neighbor_cfg.update({'strict-capability-match': neighbor['strict_capability_match']}) + if neighbor.get('ttl_security', None) is not None: + neighbor_cfg.update({'ttl-security-hops': neighbor['ttl_security']}) + if neighbor.get('v6only', None) is not None: + neighbor_cfg.update({'openconfig-bgp-ext:v6only': neighbor['v6only']}) + if neighbor.get('local_as', None) is not None: + if neighbor['local_as'].get('as', None) is not None: + neighbor_cfg.update({'local-as': neighbor['local_as']['as']}) + if neighbor['local_as'].get('no_prepend', None) is not None: + neighbor_cfg.update({'local-as-no-prepend': neighbor['local_as']['no_prepend']}) + if neighbor['local_as'].get('replace_as', None) is not None: + neighbor_cfg.update({'local-as-replace-as': neighbor['local_as']['replace_as']}) + if neighbor.get('local_address', None) is not None: + tmp_transport.update({'local-address': neighbor['local_address']}) + if neighbor.get('passive', None) is not None: + tmp_transport.update({'passive-mode': neighbor['passive']}) + if neighbor.get('remote_as', None) is not None: + have_nei = self.find_nei(have, bgp_as, vrf_name, neighbor) + if neighbor['remote_as'].get('peer_as', None) is not None: + if have_nei: + if have_nei.get("remote_as", None) is not None: + if have_nei["remote_as"].get("peer_type", None) is not None: + del_nei = {} + del_nei.update({'neighbor': have_nei['neighbor']}) + del_nei.update({'remote_as': have_nei['remote_as']}) + requests.extend(self.delete_specific_param_request(vrf_name, del_nei)) + tmp_remote.update({'peer-as': neighbor['remote_as']['peer_as']}) + if neighbor['remote_as'].get('peer_type', None) is not None: + if have_nei: + if have_nei.get("remote_as", None) is not None: + if have_nei["remote_as"].get("peer_as", None) is not None: + del_nei = {} + del_nei.update({'neighbor': have_nei['neighbor']}) + del_nei.update({'remote_as': have_nei['remote_as']}) + requests.extend(self.delete_specific_param_request(vrf_name, del_nei)) + tmp_remote.update({'peer-type': neighbor['remote_as']['peer_type'].upper()}) + if tmp_bfd: + bgp_neighbor.update({'enable-bfd': {'config': tmp_bfd}}) + if tmp_ebgp: + bgp_neighbor.update({'ebgp-multihop': {'config': tmp_ebgp}}) + if tmp_timers: + bgp_neighbor.update({'timers': {'config': tmp_timers}}) + if tmp_transport: + bgp_neighbor.update({'transport': {'config': tmp_transport}}) + if tmp_capability: + neighbor_cfg.update(tmp_capability) + if tmp_remote: + neighbor_cfg.update(tmp_remote) + if neighbor_cfg: + bgp_neighbor.update({'config': neighbor_cfg}) + if bgp_neighbor: + bgp_neighbor_list.append(bgp_neighbor) + payload = {'openconfig-network-instance:neighbors': {'neighbor': bgp_neighbor_list}} + return payload, requests + + def get_modify_bgp_requests(self, commands, have): + requests = [] + if not commands: + return requests + + for cmd in commands: + edit_path = '%s=%s/%s' % (self.network_instance_path, cmd['vrf_name'], self.protocol_bgp_path) + if 'peer_group' in cmd and cmd['peer_group']: + edit_peer_groups_payload, edit_requests = self.build_bgp_peer_groups_payload(cmd['peer_group'], have, cmd['bgp_as'], cmd['vrf_name']) + edit_peer_groups_path = edit_path + '/peer-groups' + if edit_requests: + requests.extend(edit_requests) + requests.append({'path': edit_peer_groups_path, 'method': PATCH, 'data': edit_peer_groups_payload}) + if 'neighbors' in cmd and cmd['neighbors']: + edit_neighbors_payload, edit_requests = self.build_bgp_neighbors_payload(cmd['neighbors'], have, cmd['bgp_as'], cmd['vrf_name']) + edit_neighbors_path = edit_path + '/neighbors' + if edit_requests: + requests.extend(edit_requests) + requests.append({'path': edit_neighbors_path, 'method': PATCH, 'data': edit_neighbors_payload}) + return requests + + def get_delete_specific_bgp_peergroup_param_request(self, vrf_name, cmd, want_match): + requests = [] + want_peer_group = want_match.get('peer_group', None) + for each in cmd['peer_group']: + if each: + name = each.get('name', None) + remote_as = each.get('remote_as', None) + timers = each.get('timers', None) + advertisement_interval = each.get('advertisement_interval', None) + bfd = each.get('bfd', None) + capability = each.get('capability', None) + address_family = each.get('address_family', None) + if name and not remote_as and not timers and not advertisement_interval and not bfd and not capability and not address_family: + want_pg_match = None + if want_peer_group: + want_pg_match = next((cfg for cfg in want_peer_group if cfg['name'] == name), None) + if want_pg_match: + keys = ['remote_as', 'timers', 'advertisement_interval', 'bfd', 'capability', 'address_family'] + if not any(want_pg_match.get(key, None) for key in keys): + requests.append(self.get_delete_vrf_specific_peergroup_request(vrf_name, name)) + else: + requests.extend(self.delete_specific_peergroup_param_request(vrf_name, each)) + return requests + + def delete_specific_peergroup_param_request(self, vrf_name, cmd): + requests = [] + delete_static_path = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + delete_static_path = delete_static_path + '/peer-groups/peer-group=%s' % (cmd['name']) + if cmd.get('remote_as', None) is not None: + if cmd['remote_as'].get('peer_as', None) is not None: + delete_path = delete_static_path + '/config/peer-as' + requests.append({'path': delete_path, 'method': DELETE}) + elif cmd['remote_as'].get('peer_type', None) is not None: + delete_path = delete_static_path + '/config/peer-type' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('advertisement_interval', None) is not None: + delete_path = delete_static_path + '/timers/config/minimum-advertisement-interval' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('timers', None) is not None: + if cmd['timers'].get('holdtime', None) is not None: + delete_path = delete_static_path + '/timers/config/hold-time' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['timers'].get('keepalive', None) is not None: + delete_path = delete_static_path + '/timers/config/keepalive-interval' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['timers'].get('connect_retry', None) is not None: + delete_path = delete_static_path + '/timers/config/connect-retry' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('capability', None) is not None: + if cmd['capability'].get('dynamic', None) is not None: + delete_path = delete_static_path + '/config/capability-dynamic' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['capability'].get('extended_nexthop', None) is not None: + delete_path = delete_static_path + '/config/capability-extended-nexthop' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('pg_description', None) is not None: + delete_path = delete_static_path + '/config/description' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('disable_connected_check', None) is not None: + delete_path = delete_static_path + '/config/disable-ebgp-connected-route-check' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('dont_negotiate_capability', None) is not None: + delete_path = delete_static_path + '/config/dont-negotiate-capability' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('enforce_first_as', None) is not None: + delete_path = delete_static_path + '/config/enforce-first-as' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('enforce_multihop', None) is not None: + delete_path = delete_static_path + '/config/enforce-multihop' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('override_capability', None) is not None: + delete_path = delete_static_path + '/config/override-capability' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('shutdown_msg', None) is not None: + delete_path = delete_static_path + '/config/shutdown-message' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('solo', None) is not None: + delete_path = delete_static_path + '/config/solo-peer' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('strict_capability_match', None) is not None: + delete_path = delete_static_path + '/config/strict-capability-match' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('ttl_security', None) is not None: + delete_path = delete_static_path + '/config/ttl-security-hops' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('local_as', None) is not None: + if cmd['local_as'].get('as', None) is not None: + delete_path = delete_static_path + '/config/local-as' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['local_as'].get('no_prepend', None) is not None: + delete_path = delete_static_path + '/config/local-as-no-prepend' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['local_as'].get('replace_as', None) is not None: + delete_path = delete_static_path + '/config/local-as-replace-as' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('local_address', None) is not None: + delete_path = delete_static_path + '/transport/config/local-address' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('passive', None) is not None: + delete_path = delete_static_path + '/transport/config/passive-mode' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('bfd', None) is not None: + if cmd['bfd'].get('enabled', None) is not None: + delete_path = delete_static_path + '/enable-bfd/config/enabled' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['bfd'].get('check_failure', None) is not None: + delete_path = delete_static_path + '/enable-bfd/config/check-control-plane-failure' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['bfd'].get('profile', None) is not None: + delete_path = delete_static_path + '/enable-bfd/config/bfd-profile' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('auth_pwd', None) is not None: + if cmd['auth_pwd'].get('pwd', None) is not None: + delete_path = delete_static_path + '/auth-password/config/password' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['auth_pwd'].get('encrypted', None) is not None: + delete_path = delete_static_path + '/auth-password/config/encrypted' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('ebgp_multihop', None) is not None: + if cmd['ebgp_multihop'].get('enabled', None) is not None: + delete_path = delete_static_path + '/ebgp-multihop/config/enabled' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['ebgp_multihop'].get('multihop_ttl', None) is not None: + delete_path = delete_static_path + '/ebgp-multihop/config/multihop_ttl' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('address_family', None) is not None: + if cmd['address_family'].get('afis', None) is None: + delete_path = delete_static_path + '/afi-safis/afi-safi' + requests.append({'path': delete_path, 'method': DELETE}) + else: + for each in cmd['address_family']['afis']: + afi = each.get('afi', None) + safi = each.get('safi', None) + activate = each.get('activate', None) + allowas_in = each.get('allowas_in', None) + ip_afi = each.get('ip_afi', None) + prefix_limit = each.get('prefix_limit', None) + prefix_list_in = each.get('prefix_list_in', None) + prefix_list_out = each.get('prefix_list_out', None) + afi_safi = afi.upper() + '_' + safi.upper() + afi_safi_name = 'openconfig-bgp-types:' + afi_safi + if (afi and safi and not activate and not allowas_in and not ip_afi and not prefix_limit and not prefix_list_in + and not prefix_list_out): + delete_path = delete_static_path + '/afi-safis/afi-safi=%s' % (afi_safi_name) + requests.append({'path': delete_path, 'method': DELETE}) + else: + if activate: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/config/enabled' % (afi_safi_name) + requests.append({'path': delete_path, 'method': DELETE}) + if allowas_in: + if allowas_in.get('origin', None): + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/allow-own-as/config/origin' % (afi_safi_name) + requests.append({'path': delete_path, 'method': DELETE}) + if allowas_in.get('value', None): + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/allow-own-as/config/as-count' % (afi_safi_name) + requests.append({'path': delete_path, 'method': DELETE}) + if prefix_list_in: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/prefix-list/config/import-policy' % (afi_safi_name) + requests.append({'path': delete_path, 'method': DELETE}) + if prefix_list_out: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/prefix-list/config/export-policy' % (afi_safi_name) + requests.append({'path': delete_path, 'method': DELETE}) + if afi_safi == 'IPV4_UNICAST': + if ip_afi: + requests.extend(self.delete_ip_afi_requests(ip_afi, afi_safi_name, 'ipv4-unicast', delete_static_path)) + if prefix_limit: + requests.extend(self.delete_prefix_limit_requests(prefix_limit, afi_safi_name, 'ipv4-unicast', delete_static_path)) + elif afi_safi == 'IPV6_UNICAST': + if ip_afi: + requests.extend(self.delete_ip_afi_requests(ip_afi, afi_safi_name, 'ipv6-unicast', delete_static_path)) + if prefix_limit: + requests.extend(self.delete_prefix_limit_requests(prefix_limit, afi_safi_name, 'ipv6-unicast', delete_static_path)) + elif afi_safi == 'L2VPN_EVPN': + if prefix_limit: + requests.extend(self.delete_prefix_limit_requests(prefix_limit, afi_safi_name, 'l2vpn-evpn', delete_static_path)) + + return requests + + def delete_ip_afi_requests(self, ip_afi, afi_safi_name, afi_safi, delete_static_path): + requests = [] + default_policy_name = ip_afi.get('default_policy_name', None) + send_default_route = ip_afi.get('send_default_route', None) + if default_policy_name: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/%s/config/default-policy-name' % (afi_safi_name, afi_safi) + requests.append({'path': delete_path, 'method': DELETE}) + if send_default_route: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/%s/config/send_default_route' % (afi_safi_name, afi_safi) + requests.append({'path': delete_path, 'method': DELETE}) + + return requests + + def delete_prefix_limit_requests(self, prefix_limit, afi_safi_name, afi_safi, delete_static_path): + requests = [] + max_prefixes = prefix_limit.get('max_prefixes', None) + prevent_teardown = prefix_limit.get('prevent_teardown', None) + warning_threshold = prefix_limit.get('warning_threshold', None) + restart_timer = prefix_limit.get('restart_timer', None) + if max_prefixes: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/%s/prefix-limit/config/max-prefixes' % (afi_safi_name, afi_safi) + requests.append({'path': delete_path, 'method': DELETE}) + if prevent_teardown: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/%s/prefix-limit/config/prevent-teardown' % (afi_safi_name, afi_safi) + requests.append({'path': delete_path, 'method': DELETE}) + if warning_threshold: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/%s/prefix-limit/config/warning-threshold-pct' % (afi_safi_name, afi_safi) + requests.append({'path': delete_path, 'method': DELETE}) + if restart_timer: + delete_path = delete_static_path + '/afi-safis/afi-safi=%s/%s/prefix-limit/config/restart-timer' % (afi_safi_name, afi_safi) + requests.append({'path': delete_path, 'method': DELETE}) + + return requests + + def get_delete_specific_bgp_param_request(self, vrf_name, cmd, want_match): + requests = [] + want_neighbors = want_match.get('neighbors', None) + for each in cmd['neighbors']: + if each: + neighbor = each.get('neighbor', None) + remote_as = each.get('remote_as', None) + peer_group = each.get('peer_group', None) + timers = each.get('timers', None) + advertisement_interval = each.get('advertisement_interval', None) + bfd = each.get('bfd', None) + capability = each.get('capability', None) + if neighbor and not remote_as and not peer_group and not timers and not advertisement_interval and not bfd and not capability: + want_nei_match = None + if want_neighbors: + want_nei_match = next(cfg for cfg in want_neighbors if cfg['neighbor'] == neighbor) + if want_nei_match: + keys = ['remote_as', 'peer_group', 'timers', 'advertisement_interval', 'bfd', 'capability'] + if not any(want_nei_match.get(key, None) for key in keys): + requests.append(self.delete_neighbor_whole_request(vrf_name, neighbor)) + else: + requests.extend(self.delete_specific_param_request(vrf_name, each)) + return requests + + def delete_neighbor_whole_request(self, vrf_name, neighbor): + requests = [] + url = '%s=%s/%s/%s=%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, neighbor) + return ({'path': url, 'method': DELETE}) + + def delete_specific_param_request(self, vrf_name, cmd): + requests = [] + delete_static_path = '%s=%s/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) + delete_static_path = delete_static_path + '/neighbors/neighbor=%s' % (cmd['neighbor']) + if cmd.get('remote_as', None) is not None: + if cmd['remote_as'].get('peer_as', None) is not None: + delete_path = delete_static_path + '/config/peer-as' + requests.append({'path': delete_path, 'method': DELETE}) + elif cmd['remote_as'].get('peer_type', None) is not None: + delete_path = delete_static_path + '/config/peer-type' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('peer_group', None) is not None: + delete_path = delete_static_path + '/config/peer-group' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('nbr_description', None) is not None: + delete_path = delete_static_path + '/config/description' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('disable_connected_check', None) is not None: + delete_path = delete_static_path + '/config/disable-ebgp-connected-route-check' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('dont_negotiate_capability', None) is not None: + delete_path = delete_static_path + '/config/dont-negotiate-capability' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('enforce_first_as', None) is not None: + delete_path = delete_static_path + '/config/enforce-first-as' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('enforce_multihop', None) is not None: + delete_path = delete_static_path + '/config/enforce-multihop' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('override_capability', None) is not None: + delete_path = delete_static_path + '/config/override-capability' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('port', None) is not None: + delete_path = delete_static_path + '/config/peer-port' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('shutdown_msg', None) is not None: + delete_path = delete_static_path + '/config/shutdown-message' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('solo', None) is not None: + delete_path = delete_static_path + '/config/solo-peer' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('strict_capability_match', None) is not None: + delete_path = delete_static_path + '/config/strict-capability-match' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('ttl_security', None) is not None: + delete_path = delete_static_path + '/config/ttl-security-hops' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('v6only', None) is not None: + delete_path = delete_static_path + '/config/openconfig-bgp-ext:v6only' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('local_as', None) is not None: + if cmd['local_as'].get('as', None) is not None: + delete_path = delete_static_path + '/config/local-as' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['local_as'].get('no_prepend', None) is not None: + delete_path = delete_static_path + '/config/local-as-no-prepend' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['local_as'].get('replace_as', None) is not None: + delete_path = delete_static_path + '/config/local-as-replace-as' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('local_address', None) is not None: + delete_path = delete_static_path + '/transport/config/local-address' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('passive', None) is not None: + delete_path = delete_static_path + '/transport/config/passive-mode' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('advertisement_interval', None) is not None: + delete_path = delete_static_path + '/timers/config/minimum-advertisement-interval' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('timers', None) is not None: + if cmd['timers'].get('holdtime', None) is not None: + delete_path = delete_static_path + '/timers/config/hold-time' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['timers'].get('keepalive', None) is not None: + delete_path = delete_static_path + '/timers/config/keepalive-interval' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['timers'].get('connect_retry', None) is not None: + delete_path = delete_static_path + '/timers/config/connect-retry' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('capability', None) is not None: + if cmd['capability'].get('dynamic', None) is not None: + delete_path = delete_static_path + '/config/capability-dynamic' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['capability'].get('extended_nexthop', None) is not None: + delete_path = delete_static_path + '/config/capability-extended-nexthop' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('bfd', None) is not None: + if cmd['bfd'].get('enabled', None) is not None: + delete_path = delete_static_path + '/enable-bfd/config/enabled' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['bfd'].get('check_failure', None) is not None: + delete_path = delete_static_path + '/enable-bfd/config/check-control-plane-failure' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['bfd'].get('profile', None) is not None: + delete_path = delete_static_path + '/enable-bfd/config/bfd-profile' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('auth_pwd', None) is not None: + if cmd['auth_pwd'].get('pwd', None) is not None: + delete_path = delete_static_path + '/auth-password/config/password' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['auth_pwd'].get('encrypted', None) is not None: + delete_path = delete_static_path + '/auth-password/config/encrypted' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd.get('ebgp_multihop', None) is not None: + if cmd['ebgp_multihop'].get('enabled', None) is not None: + delete_path = delete_static_path + '/ebgp-multihop/config/enabled' + requests.append({'path': delete_path, 'method': DELETE}) + if cmd['ebgp_multihop'].get('multihop_ttl', None) is not None: + delete_path = delete_static_path + '/ebgp-multihop/config/multihop_ttl' + requests.append({'path': delete_path, 'method': DELETE}) + + return requests + + def get_delete_vrf_specific_neighbor_request(self, vrf_name, have): + requests = [] + for each in have: + if each.get('neighbor', None): + requests.append(self.delete_neighbor_whole_request(vrf_name, each['neighbor'])) + return requests + + def get_delete_vrf_specific_peergroup_request(self, vrf_name, peergroup_name): + requests = [] + delete_neighbor_path = '%s=%s/%s/peer-groups/peer-group=%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, peergroup_name) + return ({'path': delete_neighbor_path, 'method': DELETE}) + + def get_delete_all_bgp_neighbor_requests(self, commands): + requests = [] + for cmd in commands: + if cmd.get('neighbors', None): + requests.extend(self.get_delete_vrf_specific_neighbor_request(cmd['vrf_name'], cmd['neighbors'])) + if 'peer_group' in cmd and cmd['peer_group']: + for each in cmd['peer_group']: + requests.append(self.get_delete_vrf_specific_peergroup_request(cmd['vrf_name'], each['name'])) + return requests + + def get_delete_bgp_neighbor_requests(self, commands, have, want, is_delete_all): + requests = [] + if is_delete_all: + requests = self.get_delete_all_bgp_neighbor_requests(commands) + else: + for cmd in commands: + vrf_name = cmd['vrf_name'] + as_val = cmd['bgp_as'] + neighbors = cmd.get('neighbors', None) + peer_group = cmd.get('peer_group', None) + want_match = next((cfg for cfg in want if vrf_name == cfg['vrf_name'] and as_val == cfg['bgp_as']), None) + want_neighbors = want_match.get('neighbors', None) + want_peer_group = want_match.get('peer_group', None) + if neighbors is None and peer_group is None and want_neighbors is None and want_peer_group is None: + new_cmd = {} + for each in have: + if vrf_name == each['vrf_name'] and as_val == each['bgp_as']: + new_neighbors = [] + new_pg = [] + if each.get('neighbors', None): + new_neighbors = [{'neighbor': i['neighbor']} for i in each.get('neighbors', None)] + if each.get('peer_group', None): + new_pg = [{'name': i['name']} for i in each.get('peer_group', None)] + if new_neighbors: + new_cmd['neighbors'] = new_neighbors + requests.extend(self.get_delete_vrf_specific_neighbor_request(vrf_name, new_cmd['neighbors'])) + if new_pg: + new_cmd['name'] = new_pg + for each in new_cmd['name']: + requests.append(self.get_delete_vrf_specific_peergroup_request(vrf_name, each['name'])) + break + else: + if neighbors: + requests.extend(self.get_delete_specific_bgp_param_request(vrf_name, cmd, want_match)) + if peer_group: + requests.extend(self.get_delete_specific_bgp_peergroup_param_request(vrf_name, cmd, want_match)) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_neighbors_af/bgp_neighbors_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_neighbors_af/bgp_neighbors_af.py new file mode 100644 index 000000000..15f46f966 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bgp_neighbors_af/bgp_neighbors_af.py @@ -0,0 +1,584 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_bgp_neighbors_af 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 + +try: + from urllib import quote +except ImportError: + from urllib.parse import quote + +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( + validate_bgps, + normalize_neighbors_interface_name, + get_ip_afi_cfg_payload, + get_prefix_limit_payload +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'config': {'vrf_name': '', 'bgp_as': ''}}, + {'neighbors': {'neighbor': ''}}, + {'address_family': {'afi': '', 'safi': ''}}, + {'route_map': {'name': '', 'direction': ''}}, +] + + +class Bgp_neighbors_af(ConfigBase): + """ + The sonic_bgp_neighbors_af class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bgp_neighbors_af', + ] + + network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' + protocol_bgp_path = 'protocols/protocol=BGP,bgp/bgp' + neighbor_path = 'neighbors/neighbor' + afi_safi_path = 'afi-safis/afi-safi' + activate_path = "/config/enabled" + ref_client_path = "/config/route-reflector-client" + serv_client_path = "/config/route-server-client" + allowas_origin_path = "/allow-own-as/config/origin" + allowas_value_path = "/allow-own-as/config/as-count" + allowas_enabled_path = "/allow-own-as/config/enabled" + prefix_list_in_path = "/prefix-list/config/import-policy" + prefix_list_out_path = "/prefix-list/config/export-policy" + def_policy_name_path = "/%s/config/default-policy-name" + send_def_route_path = "/%s/config/send-default-route" + max_prefixes_path = "/%s/prefix-limit/config/max-prefixes" + prv_teardown_path = "/%s/prefix-limit/config/prevent-teardown" + restart_timer_path = "/%s/prefix-limit/config/restart-timer" + wrn_threshold_path = "/%s/prefix-limit/config/warning-threshold-pct" + + def __init__(self, module): + super(Bgp_neighbors_af, self).__init__(module) + + def get_bgp_neighbors_af_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) + bgp_neighbors_af_facts = facts['ansible_network_resources'].get('bgp_neighbors_af') + if not bgp_neighbors_af_facts: + bgp_neighbors_af_facts = [] + return bgp_neighbors_af_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_bgp_neighbors_af_facts = self.get_bgp_neighbors_af_facts() + commands, requests = self.set_config(existing_bgp_neighbors_af_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_bgp_neighbors_af_facts = self.get_bgp_neighbors_af_facts() + + result['before'] = existing_bgp_neighbors_af_facts + if result['changed']: + result['after'] = changed_bgp_neighbors_af_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bgp_neighbors_af_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'] + normalize_neighbors_interface_name(want, self._module) + have = existing_bgp_neighbors_af_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + validate_bgps(self._module, want, have) + requests = self.get_modify_bgp_neighbors_af_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the bgp_neighbors_afs + is_delete_all = False + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests = self.get_delete_bgp_neighbors_af_requests(commands, have, is_delete_all) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def set_val(self, cfg, var, src_key, des_key): + value = var.get(src_key, None) + if value is not None: + cfg[des_key] = value + + def get_allowas_in(self, match, conf_neighbor_val, conf_afi, conf_safi): + mat_allowas_in = None + if match: + mat_neighbors = match.get('neighbors', None) + if mat_neighbors: + mat_neighbor = next((nei for nei in mat_neighbors if nei['neighbor'] == conf_neighbor_val), None) + if mat_neighbor: + mat_nei_addr_fams = mat_neighbor.get('address_family', []) + if mat_nei_addr_fams: + mat_nei_addr_fam = next((af for af in mat_nei_addr_fams if (af['afi'] == conf_afi and af['safi'] == conf_safi)), None) + if mat_nei_addr_fam: + mat_allowas_in = mat_nei_addr_fam.get('allowas_in', None) + return mat_allowas_in + + def get_single_neighbors_af_modify_request(self, match, vrf_name, conf_neighbor_val, conf_neighbor): + requests = [] + conf_nei_addr_fams = conf_neighbor.get('address_family', []) + url = '%s=%s/%s/%s=%s/afi-safis' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, conf_neighbor_val) + payload = {} + afi_safis = [] + if not conf_nei_addr_fams: + return requests + + for conf_nei_addr_fam in conf_nei_addr_fams: + afi_safi = {} + conf_afi = conf_nei_addr_fam.get('afi', None) + conf_safi = conf_nei_addr_fam.get('safi', None) + afi_safi_val = ("%s_%s" % (conf_afi, conf_safi)).upper() + del_url = '%s=%s/%s/%s=%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, conf_neighbor_val) + del_url += '%s=openconfig-bgp-types:%s' % (self.afi_safi_path, afi_safi_val) + + afi_safi_cfg = {} + if conf_afi and conf_safi: + afi_safi_name = ("%s_%s" % (conf_afi, conf_safi)).upper() + afi_safi['afi-safi-name'] = afi_safi_name + afi_safi_cfg['afi-safi-name'] = afi_safi_name + + self.set_val(afi_safi_cfg, conf_nei_addr_fam, 'activate', 'enabled') + self.set_val(afi_safi_cfg, conf_nei_addr_fam, 'route_reflector_client', 'route-reflector-client') + self.set_val(afi_safi_cfg, conf_nei_addr_fam, 'route_server_client', 'route-server-client') + + if afi_safi_cfg: + afi_safi['config'] = afi_safi_cfg + + policy_cfg = {} + conf_route_map = conf_nei_addr_fam.get('route_map', None) + if conf_route_map: + for route in conf_route_map: + policy_key = "import-policy" if "in" == route['direction'] else "export-policy" + route_name = route['name'] + policy_cfg[policy_key] = [route_name] + if policy_cfg: + afi_safi['apply-policy'] = {'config': policy_cfg} + + pfx_lst_cfg = {} + conf_prefix_list_in = conf_nei_addr_fam.get('prefix_list_in', None) + conf_prefix_list_out = conf_nei_addr_fam.get('prefix_list_out', None) + if conf_prefix_list_in: + pfx_lst_cfg['import-policy'] = conf_prefix_list_in + if conf_prefix_list_out: + pfx_lst_cfg['export-policy'] = conf_prefix_list_out + if pfx_lst_cfg: + afi_safi['prefix-list'] = {'config': pfx_lst_cfg} + + ip_dict = {} + ip_afi_cfg = {} + pfx_lmt_cfg = {} + conf_ip_afi = conf_nei_addr_fam.get('ip_afi') + conf_prefix_limit = conf_nei_addr_fam.get('prefix_limit') + if conf_prefix_limit: + pfx_lmt_cfg = get_prefix_limit_payload(conf_prefix_limit) + if pfx_lmt_cfg and afi_safi_val == 'L2VPN_EVPN': + afi_safi['l2vpn-evpn'] = {'prefix-limit': {'config': pfx_lmt_cfg}} + else: + if conf_ip_afi: + ip_afi_cfg = get_ip_afi_cfg_payload(conf_ip_afi) + if ip_afi_cfg: + ip_dict['config'] = ip_afi_cfg + if pfx_lmt_cfg: + ip_dict['prefix-limit'] = {'config': pfx_lmt_cfg} + if ip_dict and afi_safi_val == 'IPV4_UNICAST': + afi_safi['ipv4-unicast'] = ip_dict + elif ip_dict and afi_safi_val == 'IPV6_UNICAST': + afi_safi['ipv6-unicast'] = ip_dict + + allowas_in_cfg = {} + conf_allowas_in = conf_nei_addr_fam.get('allowas_in', None) + if conf_allowas_in: + mat_allowas_in = self.get_allowas_in(match, conf_neighbor_val, conf_afi, conf_safi) + origin = conf_allowas_in.get('origin', None) + if origin is not None: + if mat_allowas_in: + mat_value = mat_allowas_in.get('value', None) + if mat_value: + self.append_delete_request(requests, mat_value, mat_allowas_in, 'value', del_url, self.allowas_value_path) + allowas_in_cfg['origin'] = origin + else: + value = conf_allowas_in.get('value', None) + if value is not None: + if mat_allowas_in: + mat_origin = mat_allowas_in.get('origin', None) + if mat_origin: + self.append_delete_request(requests, mat_origin, mat_allowas_in, 'origin', del_url, self.allowas_origin_path) + allowas_in_cfg['as-count'] = value + if allowas_in_cfg: + allowas_in_cfg['enabled'] = True + afi_safi['allow-own-as'] = {'config': allowas_in_cfg} + + if afi_safi: + afi_safis.append(afi_safi) + + if afi_safis: + payload = {"openconfig-network-instance:afi-safis": {"afi-safi": afi_safis}} + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + return requests + + def get_delete_neighbor_af_routemaps_requests(self, vrf_name, conf_neighbor_val, afi, safi, routes): + requests = [] + for route in routes: + afi_safi_name = ("%s_%s" % (afi, safi)).upper() + policy_type = "import-policy" if "in" == route['direction'] else "export-policy" + url = '%s=%s/%s/%s=%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, conf_neighbor_val) + url += ('%s=%s/apply-policy/config/%s' % (self.afi_safi_path, afi_safi_name, policy_type)) + requests.append({'path': url, 'method': DELETE}) + return requests + + def get_all_neighbors_af_modify_requests(self, match, conf_neighbors, vrf_name): + requests = [] + for conf_neighbor in conf_neighbors: + conf_neighbor_val = conf_neighbor.get('neighbor', None) + if conf_neighbor_val: + requests.extend(self.get_single_neighbors_af_modify_request(match, vrf_name, conf_neighbor_val, conf_neighbor)) + return requests + + def get_modify_requests(self, conf, match, vrf_name): + requests = [] + conf_neighbors = conf.get('neighbors', []) + mat_neighbors = [] + if match and match.get('neighbors', None): + mat_neighbors = match.get('neighbors') + + if conf_neighbors: + for conf_neighbor in conf_neighbors: + conf_neighbor_val = conf_neighbor.get('neighbor', None) + if conf_neighbor_val is None: + continue + + mat_neighbor = next((e_neighbor for e_neighbor in mat_neighbors if e_neighbor['neighbor'] == conf_neighbor_val), None) + if mat_neighbor is None: + continue + + conf_nei_addr_fams = conf_neighbor.get('address_family', None) + mat_nei_addr_fams = mat_neighbor.get('address_family', None) + if conf_nei_addr_fams is None or mat_nei_addr_fams is None: + continue + + for conf_nei_addr_fam in conf_nei_addr_fams: + afi = conf_nei_addr_fam.get('afi', None) + safi = conf_nei_addr_fam.get('safi', None) + if afi is None or safi is None: + continue + + mat_nei_addr_fam = next((addr_fam for addr_fam in mat_nei_addr_fams if (addr_fam['afi'] == afi and addr_fam['safi'] == safi)), None) + if mat_nei_addr_fam is None: + continue + + conf_route_map = conf_nei_addr_fam.get('route_map', None) + mat_route_map = mat_nei_addr_fam.get('route_map', None) + if conf_route_map is None or mat_route_map is None: + continue + + del_routes = [] + for route in conf_route_map: + exist_route = next((e_route for e_route in mat_route_map if e_route['direction'] == route['direction']), None) + if exist_route: + del_routes.append(exist_route) + if del_routes: + requests.extend(self.get_delete_neighbor_af_routemaps_requests(vrf_name, conf_neighbor_val, afi, safi, del_routes)) + + requests.extend(self.get_all_neighbors_af_modify_requests(match, conf_neighbors, vrf_name)) + return requests + + def get_modify_bgp_neighbors_af_requests(self, commands, have): + requests = [] + if not commands: + return requests + + # Create URL and payload + for conf in commands: + vrf_name = conf['vrf_name'] + as_val = conf['bgp_as'] + + match = next((cfg for cfg in have if (cfg['vrf_name'] == vrf_name and (cfg['bgp_as'] == as_val))), None) + modify_reqs = self.get_modify_requests(conf, match, vrf_name) + if modify_reqs: + requests.extend(modify_reqs) + + return requests + + def append_delete_request(self, requests, cur_var, mat_var, key, url, path): + ret_value = False + request = None + if cur_var is not None and mat_var.get(key, None): + requests.append({'path': url + path, 'method': DELETE}) + ret_value = True + return ret_value + + def delete_ip_afi_requests(self, conf_ip_afi, mat_ip_afi, conf_afi_safi_val, url): + requests = [] + default_policy_name = conf_ip_afi.get('default_policy_name', None) + send_default_route = conf_ip_afi.get('send_default_route', None) + if default_policy_name: + self.append_delete_request(requests, default_policy_name, mat_ip_afi, 'default_policy_name', url, self.def_policy_name_path % (conf_afi_safi_val)) + if send_default_route: + self.append_delete_request(requests, send_default_route, mat_ip_afi, 'send_default_route', url, self.send_def_route_path % (conf_afi_safi_val)) + + return requests + + def delete_prefix_limit_requests(self, conf_prefix_limit, mat_prefix_limit, conf_afi_safi_val, url): + requests = [] + max_prefixes = conf_prefix_limit.get('max_prefixes', None) + prevent_teardown = conf_prefix_limit.get('prevent_teardown', None) + restart_timer = conf_prefix_limit.get('restart_timer', None) + warning_threshold = conf_prefix_limit.get('warning_threshold', None) + if max_prefixes: + self.append_delete_request(requests, max_prefixes, mat_prefix_limit, 'max_prefixes', url, self.max_prefixes_path % (conf_afi_safi_val)) + if prevent_teardown: + self.append_delete_request(requests, prevent_teardown, mat_prefix_limit, 'prevent_teardown', url, self.prv_teardown_path % (conf_afi_safi_val)) + if restart_timer: + self.append_delete_request(requests, restart_timer, mat_prefix_limit, 'restart_timer', url, self.restart_timer_path % (conf_afi_safi_val)) + if warning_threshold: + self.append_delete_request(requests, warning_threshold, mat_prefix_limit, 'warning_threshold', url, self.wrn_threshold_path % (conf_afi_safi_val)) + + return requests + + def process_delete_specific_params(self, vrf_name, conf_neighbor_val, conf_nei_addr_fam, conf_afi, conf_safi, matched_nei_addr_fams, url): + requests = [] + conf_afi_safi_val = ("%s-%s" % (conf_afi, conf_safi)) + + mat_nei_addr_fam = None + if matched_nei_addr_fams: + mat_nei_addr_fam = next((e_af for e_af in matched_nei_addr_fams if (e_af['afi'] == conf_afi and e_af['safi'] == conf_safi)), None) + + if mat_nei_addr_fam: + conf_alllowas_in = conf_nei_addr_fam.get('allowas_in', None) + conf_activate = conf_nei_addr_fam.get('activate', None) + conf_route_map = conf_nei_addr_fam.get('route_map', None) + conf_route_reflector_client = conf_nei_addr_fam.get('route_reflector_client', None) + conf_route_server_client = conf_nei_addr_fam.get('route_server_client', None) + conf_prefix_list_in = conf_nei_addr_fam.get('prefix_list_in', None) + conf_prefix_list_out = conf_nei_addr_fam.get('prefix_list_out', None) + conf_ip_afi = conf_nei_addr_fam.get('ip_afi', None) + conf_prefix_limit = conf_nei_addr_fam.get('prefix_limit', None) + + var_list = [conf_alllowas_in, conf_activate, conf_route_map, conf_route_reflector_client, conf_route_server_client, + conf_prefix_list_in, conf_prefix_list_out, conf_ip_afi, conf_prefix_limit] + if len(list(filter(lambda var: (var is None), var_list))) == len(var_list): + requests.append({'path': url, 'method': DELETE}) + else: + mat_route_map = mat_nei_addr_fam.get('route_map', None) + if conf_route_map and mat_route_map: + del_routes = [] + for route in conf_route_map: + if any(e_route for e_route in mat_route_map if route['direction'] == e_route['direction']): + del_routes.append(route) + if del_routes: + requests.extend(self.get_delete_neighbor_af_routemaps_requests(vrf_name, conf_neighbor_val, conf_afi, conf_safi, del_routes)) + + self.append_delete_request(requests, conf_activate, mat_nei_addr_fam, 'activate', url, self.activate_path) + self.append_delete_request(requests, conf_route_reflector_client, mat_nei_addr_fam, 'route_reflector_client', url, self.ref_client_path) + self.append_delete_request(requests, conf_route_server_client, mat_nei_addr_fam, 'route_server_client', url, self.serv_client_path) + self.append_delete_request(requests, conf_prefix_list_in, mat_nei_addr_fam, 'prefix_list_in', url, self.prefix_list_in_path) + self.append_delete_request(requests, conf_prefix_list_out, mat_nei_addr_fam, 'prefix_list_out', url, self.prefix_list_out_path) + + mat_alllowas_in = mat_nei_addr_fam.get('allowas_in', None) + if conf_alllowas_in is not None and mat_alllowas_in: + origin = conf_alllowas_in.get('origin', None) + if origin is not None: + if self.append_delete_request(requests, origin, mat_alllowas_in, 'origin', url, self.allowas_origin_path): + self.append_delete_request(requests, True, {'enabled': True}, 'enabled', url, self.allowas_enabled_path) + else: + value = conf_alllowas_in.get('value', None) + if value is not None: + if self.append_delete_request(requests, value, mat_alllowas_in, 'value', url, self.allowas_value_path): + self.append_delete_request(requests, True, {'enabled': True}, 'enabled', url, self.allowas_enabled_path) + + mat_ip_afi = mat_nei_addr_fam.get('ip_afi', None) + mat_prefix_limit = mat_nei_addr_fam.get('prefix_limit', None) + if conf_ip_afi and mat_ip_afi: + requests.extend(self.delete_ip_afi_requests(conf_ip_afi, mat_ip_afi, conf_afi_safi_val, url)) + if conf_prefix_limit and mat_prefix_limit: + requests.extend(self.delete_prefix_limit_requests(conf_prefix_limit, mat_prefix_limit, conf_afi_safi_val, url)) + + return requests + + def process_neighbor_delete_address_families(self, vrf_name, conf_nei_addr_fams, matched_nei_addr_fams, neighbor_val, is_delete_all): + requests = [] + + for conf_nei_addr_fam in conf_nei_addr_fams: + conf_afi = conf_nei_addr_fam.get('afi', None) + conf_safi = conf_nei_addr_fam.get('safi', None) + if not conf_afi or not conf_safi: + continue + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + url = '%s=%s/%s/%s=%s/' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.neighbor_path, neighbor_val) + url += '%s=openconfig-bgp-types:%s' % (self.afi_safi_path, afi_safi) + if is_delete_all: + requests.append({'path': url, 'method': DELETE}) + else: + requests.extend(self.process_delete_specific_params(vrf_name, neighbor_val, conf_nei_addr_fam, conf_afi, conf_safi, matched_nei_addr_fams, url)) + + return requests + + def get_delete_single_bgp_neighbors_af_request(self, conf, is_delete_all, match=None): + requests = [] + vrf_name = conf['vrf_name'] + conf_neighbors = conf.get('neighbors', []) + + if match and not conf_neighbors: + conf_neighbors = match.get('neighbors', []) + if conf_neighbors: + conf_neighbors = [{'neighbor': nei['neighbor']} for nei in conf_neighbors] + + if not conf_neighbors: + return requests + mat_neighbors = None + if match: + mat_neighbors = match.get('neighbors', []) + + for conf_neighbor in conf_neighbors: + conf_neighbor_val = conf_neighbor.get('neighbor', None) + if not conf_neighbor_val: + continue + + mat_neighbor = None + if mat_neighbors: + mat_neighbor = next((e_nei for e_nei in mat_neighbors if e_nei['neighbor'] == conf_neighbor_val), None) + + conf_nei_addr_fams = conf_neighbor.get('address_family', None) + if mat_neighbor and not conf_nei_addr_fams: + conf_nei_addr_fams = mat_neighbor.get('address_family', None) + if conf_nei_addr_fams: + conf_nei_addr_fams = [{'afi': af['afi'], 'safi': af['safi']} for af in conf_nei_addr_fams] + + if not conf_nei_addr_fams: + continue + + mat_nei_addr_fams = None + if mat_neighbor: + mat_nei_addr_fams = mat_neighbor.get('address_family', None) + + requests.extend(self.process_neighbor_delete_address_families(vrf_name, conf_nei_addr_fams, mat_nei_addr_fams, conf_neighbor_val, is_delete_all)) + + return requests + + def get_delete_bgp_neighbors_af_requests(self, commands, have, is_delete_all): + requests = [] + for cmd in commands: + vrf_name = cmd['vrf_name'] + as_val = cmd['bgp_as'] + match = None + if not is_delete_all: + match = next((have_cfg for have_cfg in have if have_cfg['vrf_name'] == vrf_name and have_cfg['bgp_as'] == as_val), None) + requests.extend(self.get_delete_single_bgp_neighbors_af_request(cmd, is_delete_all, match)) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/interfaces/interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/interfaces/interfaces.py new file mode 100644 index 000000000..acf985ebf --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/interfaces/interfaces.py @@ -0,0 +1,354 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_interfaces 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 + +try: + from urllib import quote +except ImportError: + from urllib.parse import quote + +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.interfaces_util import ( + build_interfaces_create_request, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states, + normalize_interface_name +) +from ansible.module_utils._text import to_native +from ansible.module_utils.connection import ConnectionError +import traceback + +LIB_IMP_ERR = None +ERR_MSG = None +try: + import requests + HAS_LIB = True +except Exception as e: + HAS_LIB = False + ERR_MSG = to_native(e) + LIB_IMP_ERR = traceback.format_exc() + +PATCH = 'patch' +DELETE = 'delete' + + +class Interfaces(ConfigBase): + """ + The sonic_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'interfaces', + ] + + params = ('description', 'mtu', 'enabled') + delete_flag = False + + def __init__(self, module): + super(Interfaces, self).__init__(module) + + def get_interfaces_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) + interfaces_facts = facts['ansible_network_resources'].get('interfaces') + if not interfaces_facts: + return [] + + return interfaces_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_interfaces_facts = self.get_interfaces_facts() + commands, requests = self.set_config(existing_interfaces_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_interfaces_facts = self.get_interfaces_facts() + + result['before'] = existing_interfaces_facts + if result['changed']: + result['after'] = changed_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_interfaces_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'] + normalize_interface_name(want, self._module) + have = existing_interfaces_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'] + # diff method works on dict, so creating temp dict + diff = get_diff(want, have) + # removing the dict in case diff found + + if state == 'overridden': + have = [each_intf for each_intf in have if each_intf['name'].startswith('Ethernet')] + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + + return commands, requests + + def _state_replaced(self, want, have, diff): + """ The command generator when state is replaced + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param interface_type: interface type + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = self.filter_comands_to_change(diff, have) + requests = self.get_delete_interface_requests(commands, have) + requests.extend(self.get_modify_interface_requests(commands, have)) + if commands and len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have, diff): + """ The command generator when state is overridden + + :param want: the desired configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + commands_del = self.filter_comands_to_change(want, have) + requests = self.get_delete_interface_requests(commands_del, have) + del_req_count = len(requests) + if commands_del and del_req_count > 0: + commands_del = update_states(commands_del, "deleted") + commands.extend(commands_del) + + commands_over = diff + requests.extend(self.get_modify_interface_requests(commands_over, have)) + if commands_over and len(requests) > del_req_count: + commands_over = update_states(commands_over, "overridden") + commands.extend(commands_over) + + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_interface_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :param interface_type: interface type + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the interfaces + if not want: + commands = have + else: + commands = want + + requests = self.get_delete_interface_requests(commands, have) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def filter_comands_to_delete(self, configs, have): + commands = [] + + for conf in configs: + if self.is_this_delete_required(conf, have): + temp_conf = dict() + temp_conf['name'] = conf['name'] + temp_conf['description'] = '' + temp_conf['mtu'] = 9100 + temp_conf['enabled'] = True + commands.append(temp_conf) + return commands + + def filter_comands_to_change(self, configs, have): + commands = [] + if configs: + for conf in configs: + if self.is_this_change_required(conf, have): + commands.append(conf) + return commands + + def get_modify_interface_requests(self, configs, have): + self.delete_flag = False + commands = self.filter_comands_to_change(configs, have) + + return self.get_interface_requests(commands, have) + + def get_delete_interface_requests(self, configs, have): + self.delete_flag = True + commands = self.filter_comands_to_delete(configs, have) + + return self.get_interface_requests(commands, have) + + def get_interface_requests(self, configs, have): + requests = [] + if not configs: + return requests + + # Create URL and payload + for conf in configs: + name = conf["name"] + if self.delete_flag and name.startswith('Loopback'): + method = DELETE + url = 'data/openconfig-interfaces:interfaces/interface=%s' % quote(name, safe='') + request = {"path": url, "method": method} + else: + # Create Loopback in case not availble in have + if name.startswith('Loopback'): + have_conf = next((cfg for cfg in have if cfg['name'] == name), None) + if not have_conf: + loopback_create_request = build_interfaces_create_request(name) + requests.append(loopback_create_request) + method = PATCH + url = 'data/openconfig-interfaces:interfaces/interface=%s/config' % quote(name, safe='') + payload = self.build_create_payload(conf) + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def is_this_delete_required(self, conf, have): + if conf['name'] == "eth0": + return False + intf = next((e_intf for e_intf in have if conf['name'] == e_intf['name']), None) + if intf: + if (intf['name'].startswith('Loopback') or not ((intf.get('description') is None or intf.get('description') == '') and + (intf.get('enabled') is None or intf.get('enabled') is True) and (intf.get('mtu') is None or intf.get('mtu') == 9100))): + return True + return False + + def is_this_change_required(self, conf, have): + if conf['name'] == "eth0": + return False + ret_flag = False + intf = next((e_intf for e_intf in have if conf['name'] == e_intf['name']), None) + if intf: + # Check all parameter if any one is differen from existing + for param in self.params: + if conf.get(param) is not None and conf.get(param) != intf.get(param): + ret_flag = True + break + # if given interface is not present + else: + ret_flag = True + + return ret_flag + + def build_create_payload(self, conf): + temp_conf = dict() + temp_conf['name'] = conf['name'] + + if not temp_conf['name'].startswith('Loopback'): + if conf.get('enabled') is not None: + if conf.get('enabled'): + temp_conf['enabled'] = True + else: + temp_conf['enabled'] = False + if conf.get('description') is not None: + temp_conf['description'] = conf['description'] + if conf.get('mtu') is not None: + temp_conf['mtu'] = conf['mtu'] + + payload = {'openconfig-interfaces:config': temp_conf} + return payload diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_interfaces/l2_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_interfaces/l2_interfaces.py new file mode 100644 index 000000000..fccba7707 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_interfaces/l2_interfaces.py @@ -0,0 +1,414 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_l2_interfaces 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 + +import json + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( + ConfigBase +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states, + normalize_interface_name +) +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.sonic import ( + to_request, + edit_config +) +from ansible.module_utils._text import to_native +from ansible.module_utils.connection import ConnectionError +import traceback + +LIB_IMP_ERR = None +ERR_MSG = None +try: + import requests + HAS_LIB = True +except Exception as e: + HAS_LIB = False + ERR_MSG = to_native(e) + LIB_IMP_ERR = traceback.format_exc() + +PATCH = 'patch' +intf_key = 'openconfig-if-ethernet:ethernet' +port_chnl_key = 'openconfig-if-aggregate:aggregation' + +TEST_KEYS = [ + {'allowed_vlans': {'vlan': ''}}, +] + + +class L2_interfaces(ConfigBase): + """ + The sonic_l2_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'l2_interfaces', + ] + + def __init__(self, module): + super(L2_interfaces, self).__init__(module) + + def get_l2_interfaces_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) + l2_interfaces_facts = facts['ansible_network_resources'].get('l2_interfaces') + if not l2_interfaces_facts: + return [] + return l2_interfaces_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_l2_interfaces_facts = self.get_l2_interfaces_facts() + commands, requests = self.set_config(existing_l2_interfaces_facts) + + if commands: + 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_l2_interfaces_facts = self.get_l2_interfaces_facts() + + result['before'] = existing_l2_interfaces_facts + if result['changed']: + result['after'] = changed_l2_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_l2_interfaces_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'] + normalize_interface_name(want, self._module) + have = existing_l2_interfaces_facts + + for intf in have: + if not intf.get('access'): + intf.update({'access': None}) + if not intf.get('trunk'): + intf.update({'trunk': None}) + + 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'] + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + + return commands, requests + + def _state_replaced(self, want, have, diff): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + + requests = [] + commands = diff + + if commands: + requests_del = self.get_delete_all_switchport_requests(commands) + if requests_del: + requests.extend(requests_del) + + requests_rep = self.get_create_l2_interface_request(commands) + if len(requests_del) or len(requests_rep): + requests.extend(requests_rep) + commands = update_states(commands, "replaced") + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have, diff): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + commands_del = get_diff(have, want, TEST_KEYS) + requests_del = self.get_delete_all_switchport_requests(commands_del) + if len(requests_del): + requests.extend(requests_del) + commands_del = update_states(commands_del, "deleted") + commands.extend(commands_del) + + commands_over = diff + requests_over = self.get_create_l2_interface_request(commands_over) + if requests_over: + requests.extend(requests_over) + commands_over = update_states(commands_over, "overridden") + commands.extend(commands_over) + + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration at position-0 + Requests necessary to merge to the current configuration + at position-1 + """ + commands = diff + requests = self.get_create_l2_interface_request(commands) + if commands and len(requests): + commands = update_states(commands, "merged") + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + + # if want is none, then delete all the vlan links + if not want or len(have) == 0: + commands = have + requests = self.get_delete_all_switchport_requests(commands) + else: + commands = want + requests = self.get_delete_specifig_switchport_requests(want, have) + if len(requests) == 0: + commands = [] + + if commands: + commands = update_states(commands, "deleted") + + return commands, requests + + def get_trunk_delete_switchport_request(self, config, match_config): + method = "DELETE" + name = config['name'] + requests = [] + match_trunk = match_config.get('trunk') + if match_trunk: + conf_allowed_vlans = config['trunk'].get('allowed_vlans', []) + if conf_allowed_vlans: + for each_allowed_vlan in conf_allowed_vlans: + if each_allowed_vlan in match_trunk.get('allowed_vlans'): + vlan_id = each_allowed_vlan['vlan'] + key = intf_key + if name.startswith('PortChannel'): + key = port_chnl_key + url = "data/openconfig-interfaces:interfaces/interface={0}/{1}/".format(name, key) + url += "openconfig-vlan:switched-vlan/config/trunk-vlans={0}".format(vlan_id) + request = {"path": url, "method": method} + requests.append(request) + return requests + + def get_access_delete_switchport_request(self, config, match_config): + method = "DELETE" + request = None + name = config['name'] + match_access = match_config.get('access') + if match_access and match_access.get('vlan') == config['access'].get('vlan'): + key = intf_key + if name.startswith('PortChannel'): + key = port_chnl_key + url = "data/openconfig-interfaces:interfaces/interface={}/{}/openconfig-vlan:switched-vlan/config/access-vlan" + request = {"path": url.format(name, key), "method": method} + return request + + def get_delete_all_switchport_requests(self, configs): + requests = [] + if not configs: + return requests + # Create URL and payload + url = "data/openconfig-interfaces:interfaces/interface={}/{}/openconfig-vlan:switched-vlan/config" + method = "DELETE" + for intf in configs: + name = intf.get("name") + key = intf_key + if name.startswith('PortChannel'): + key = port_chnl_key + request = {"path": url.format(name, key), + "method": method, + } + requests.append(request) + + return requests + + def get_delete_specifig_switchport_requests(self, configs, have): + requests = [] + if not configs: + return requests + + for conf in configs: + name = conf['name'] + + matched = next((cnf for cnf in have if cnf['name'] == name), None) + if matched: + keys = conf.keys() + + # if both access and trunk not mention in delete + if not ('access' in keys) and not ('trunk' in keys): + requests.extend(self.get_delete_all_switchport_requests([conf])) + else: + # if access or trnuk is mentioned with value + if conf.get('access') or conf.get('trunk'): + # if access is mentioned with value + if conf.get('access'): + vlan = conf.get('access').get('vlan') + if vlan: + request = self.get_access_delete_switchport_request(conf, matched) + if request: + requests.append(request) + else: + if matched.get('access') and matched.get('access').get('vlan'): + conf['access']['vlan'] = matched.get('access').get('vlan') + request = self.get_access_delete_switchport_request(conf, matched) + if request: + requests.append(request) + + # if trunk is mentioned with value + if conf.get('trunk'): + allowed_vlans = conf['trunk'].get('allowed_vlans') + if allowed_vlans: + requests.extend(self.get_trunk_delete_switchport_request(conf, matched)) + # allowed vlans mentinoed without value + else: + if matched.get('trunk') and matched.get('trunk').get('allowed_vlans'): + conf['trunk']['allowed_vlans'] = matched.get('trunk') and matched.get('trunk').get('allowed_vlans').copy() + requests.extend(self.get_trunk_delete_switchport_request(conf, matched)) + # check for access or trunk is mentioned without value + else: + # access mentioned wothout value + if ('access' in keys) and conf.get('access', None) is None: + # get the existing values and delete it + if matched.get('access'): + conf['access'] = matched.get('access').copy() + request = self.get_access_delete_switchport_request(conf, matched) + if request: + requests.append(request) + # trunk mentioned wothout value + if ('trunk' in keys) and conf.get('trunk', None) is None: + # get the existing values and delete it + if matched.get('trunk'): + conf['trunk'] = matched.get('trunk').copy() + requests.extend(self.get_trunk_delete_switchport_request(conf, matched)) + + return requests + + def get_create_l2_interface_request(self, configs): + requests = [] + if not configs: + return requests + # Create URL and payload + url = "data/openconfig-interfaces:interfaces/interface={}/{}/openconfig-vlan:switched-vlan/config" + method = "PATCH" + for conf in configs: + name = conf.get('name') + if name == "eth0": + continue + key = intf_key + if name.startswith('PortChannel'): + key = port_chnl_key + payload = self.build_create_payload(conf) + request = {"path": url.format(name, key), + "method": method, + "data": payload + } + requests.append(request) + return requests + + def build_create_payload(self, conf): + payload_url = '{"openconfig-vlan:config":{ ' + access_payload = '' + trunk_payload = '' + if conf.get('access'): + access_vlan_id = conf['access']['vlan'] + access_payload = '"access-vlan": {0}'.format(access_vlan_id) + if conf.get('trunk'): + trunk_payload = '"trunk-vlans": [' + cnt = 0 + for each_allowed_vlan in conf['trunk']['allowed_vlans']: + if cnt > 0: + trunk_payload += ',' + trunk_payload += str(each_allowed_vlan['vlan']) + cnt = cnt + 1 + trunk_payload += ']' + + if access_payload != '': + payload_url += access_payload + if trunk_payload != '': + if access_payload != '': + payload_url += ',' + payload_url += trunk_payload + + payload_url += '}}' + + ret_payload = json.loads(payload_url) + return ret_payload diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_interfaces/l3_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_interfaces/l3_interfaces.py new file mode 100644 index 000000000..d1b735251 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_interfaces/l3_interfaces.py @@ -0,0 +1,515 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_l3_interfaces 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, + normalize_interface_name, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils._text import to_native +from ansible.module_utils.connection import ConnectionError + +TEST_KEYS = [ + {"addresses": {"address": "", "secondary": ""}} +] + +DELETE = "DELETE" +PATCH = "PATCH" + + +class L3_interfaces(ConfigBase): + """ + The sonic_l3_interfaces class + """ + + gather_subset = [ + '!all', + '!min' + ] + + gather_network_resources = [ + 'l3_interfaces', + ] + + def __init__(self, module): + super(L3_interfaces, self).__init__(module) + + def get_l3_interfaces_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) + l3_interfaces_facts = facts['ansible_network_resources'].get('l3_interfaces') + if not l3_interfaces_facts: + return [] + return l3_interfaces_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_l3_interfaces_facts = self.get_l3_interfaces_facts() + commands, requests = self.set_config(existing_l3_interfaces_facts) + if commands: + 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_l3_interfaces_facts = self.get_l3_interfaces_facts() + + result['before'] = existing_l3_interfaces_facts + if result['changed']: + result['after'] = changed_l3_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_l3_interfaces_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'] + normalize_interface_name(want, self._module) + have = existing_l3_interfaces_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'] + diff = get_diff(want, have, TEST_KEYS) + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + ret_commands = commands + return ret_commands, requests + + def _state_replaced(self, want, have, diff): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + ret_requests = list() + commands = list() + l3_interfaces_to_delete = get_diff(have, want, TEST_KEYS) + obj = self.get_object(l3_interfaces_to_delete, want) + diff = get_diff(obj, want, TEST_KEYS) + if diff: + delete_l3_interfaces_requests = self.get_delete_all_requests(want) + ret_requests.extend(delete_l3_interfaces_requests) + commands.extend(update_states(want, "deleted")) + l3_interfaces_to_create_requests = self.get_create_l3_interfaces_requests(want, have, want) + ret_requests.extend(l3_interfaces_to_create_requests) + commands.extend(update_states(want, "merged")) + return commands, ret_requests + + def _state_overridden(self, want, have, diff): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + ret_requests = list() + commands = list() + interfaces_to_delete = get_diff(have, want, TEST_KEYS) + if interfaces_to_delete: + delete_interfaces_requests = self.get_delete_l3_interfaces_requests(want, have) + ret_requests.extend(delete_interfaces_requests) + commands.extend(update_states(interfaces_to_delete, "deleted")) + + if diff: + interfaces_to_create_requests = self.get_create_l3_interfaces_requests(diff, have, want) + ret_requests.extend(interfaces_to_create_requests) + commands.extend(update_states(diff, "merged")) + + return commands, ret_requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + self.validate_primary_ips(want) + commands = diff + requests = self.get_create_l3_interfaces_requests(commands, have, want) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = list() + if not want: + commands = have + requests = self.get_delete_all_completely_requests(commands) + else: + commands = want + requests = self.get_delete_l3_interfaces_requests(commands, have) + if len(requests) == 0: + commands = [] + if commands: + commands = update_states(commands, "deleted") + return commands, requests + + def get_object(self, have, want): + objects = list() + names = [i.get('name', None) for i in want] + for obj in have: + if 'name' in obj and obj['name'] in names: + objects.append(obj.copy()) + return objects + + def get_address(self, ip_str, have_obj): + to_return = list() + for i in have_obj: + if i.get(ip_str) and i[ip_str].get('addresses'): + for ip in i[ip_str]['addresses']: + to_return.append(ip['address']) + return to_return + + def get_delete_l3_interfaces_requests(self, want, have): + requests = [] + ipv4_addrs_url_all = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv4/addresses' + ipv6_addrs_url_all = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv6/addresses' + ipv4_anycast_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv4' + ipv4_anycast_url += '/openconfig-interfaces-ext:sag-ipv4/config/static-anycast-gateway={anycast_ip}' + ipv4_addr_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv4/addresses/address={address}' + ipv6_addr_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv6/addresses/address={address}' + ipv6_enabled_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv6/config/enabled' + + for each_l3 in want: + l3 = each_l3.copy() + name = l3.pop('name') + sub_intf = self.get_sub_interface_name(name) + have_obj = next((e_cfg for e_cfg in have if e_cfg['name'] == name), None) + if not have_obj: + continue + have_ipv4_addrs = list() + have_ipv4_anycast_addrs = list() + have_ipv6_addrs = list() + have_ipv6_enabled = None + + if have_obj.get('ipv4'): + if 'addresses' in have_obj['ipv4']: + have_ipv4_addrs = have_obj['ipv4']['addresses'] + if 'anycast_addresses' in have_obj['ipv4']: + have_ipv4_anycast_addrs = have_obj['ipv4']['anycast_addresses'] + + have_ipv6_addrs = self.get_address('ipv6', [have_obj]) + if have_obj.get('ipv6') and 'enabled' in have_obj['ipv6']: + have_ipv6_enabled = have_obj['ipv6']['enabled'] + + ipv4 = l3.get('ipv4', None) + ipv6 = l3.get('ipv6', None) + + ipv4_addrs = None + ipv6_addrs = None + + is_del_ipv4 = None + is_del_ipv6 = None + if name and ipv4 is None and ipv6 is None: + is_del_ipv4 = True + is_del_ipv6 = True + elif ipv4 and ipv4.get('addresses') and not ipv4.get('anycast_addresses'): + is_del_ipv4 = True + elif ipv6 and not ipv6.get('addresses') and ipv6.get('enabled') is None: + is_del_ipv6 = True + + if is_del_ipv4: + if have_ipv4_addrs and len(have_ipv4_addrs) != 0: + ipv4_addrs_delete_request = {"path": ipv4_addrs_url_all.format(intf_name=name, sub_intf_name=sub_intf), "method": DELETE} + requests.append(ipv4_addrs_delete_request) + if have_ipv4_anycast_addrs and len(have_ipv4_anycast_addrs) != 0: + for ip in have_ipv4_anycast_addrs: + ip = ip.replace('/', '%2f') + anycast_delete_request = {"path": ipv4_anycast_url.format(intf_name=name, sub_intf_name=sub_intf, anycast_ip=ip), "method": DELETE} + requests.append(anycast_delete_request) + else: + ipv4_addrs = [] + ipv4_anycast_addrs = [] + if l3.get('ipv4'): + if l3['ipv4'].get('addresses'): + ipv4_addrs = l3['ipv4']['addresses'] + if l3['ipv4'].get('anycast_addresses'): + ipv4_anycast_addrs = l3['ipv4']['anycast_addresses'] + + # Store the primary ip at end of the list. So primary ip will be deleted after the secondary ips + ipv4_del_reqs = [] + for ip in ipv4_addrs: + match_ip = next((addr for addr in have_ipv4_addrs if addr['address'] == ip['address']), None) + if match_ip: + addr = ip['address'].split('/')[0] + del_url = ipv4_addr_url.format(intf_name=name, sub_intf_name=sub_intf, address=addr) + if match_ip['secondary']: + del_url += '/config/secondary' + ipv4_del_reqs.insert(0, {"path": del_url, "method": DELETE}) + else: + ipv4_del_reqs.append({"path": del_url, "method": DELETE}) + if ipv4_del_reqs: + requests.extend(ipv4_del_reqs) + + for ip in ipv4_anycast_addrs: + if have_ipv4_addrs and ip in have_ipv4_addrs: + ip = ip.replace('/', '%2f') + anycast_delete_request = {"path": ipv4_anycast_url.format(intf_name=name, sub_intf_name=sub_intf, anycast_ip=ip), "method": DELETE} + requests.append(anycast_delete_request) + + if is_del_ipv6: + if have_ipv6_addrs and len(have_ipv6_addrs) != 0: + ipv6_addrs_delete_request = {"path": ipv6_addrs_url_all.format(intf_name=name, sub_intf_name=sub_intf), "method": DELETE} + requests.append(ipv6_addrs_delete_request) + + if have_ipv6_enabled: + ipv6_enabled_delete_request = {"path": ipv6_enabled_url.format(intf_name=name, sub_intf_name=sub_intf), "method": DELETE} + requests.append(ipv6_enabled_delete_request) + else: + ipv6_addrs = [] + ipv6_enabled = None + if l3.get('ipv6'): + if l3['ipv6'].get('addresses'): + ipv6_addrs = l3['ipv6']['addresses'] + if 'enabled' in l3['ipv6']: + ipv6_enabled = l3['ipv6']['enabled'] + + for ip in ipv6_addrs: + if have_ipv6_addrs and ip['address'] in have_ipv6_addrs: + addr = ip['address'].split('/')[0] + request = {"path": ipv6_addr_url.format(intf_name=name, sub_intf_name=sub_intf, address=addr), "method": DELETE} + requests.append(request) + + if have_ipv6_enabled and ipv6_enabled is not None: + request = {"path": ipv6_enabled_url.format(intf_name=name, sub_intf_name=sub_intf), "method": DELETE} + requests.append(request) + return requests + + def get_delete_all_completely_requests(self, configs): + delete_requests = list() + for l3 in configs: + if l3['ipv4'] or l3['ipv6']: + delete_requests.append(l3) + return self.get_delete_all_requests(delete_requests) + + def get_delete_all_requests(self, configs): + requests = [] + ipv4_addrs_url_all = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv4/addresses' + ipv4_anycast_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv4' + ipv4_anycast_url += '/openconfig-interfaces-ext:sag-ipv4/config/static-anycast-gateway={anycast_ip}' + ipv6_addrs_url_all = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv6/addresses' + ipv6_enabled_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv6/config/enabled' + for l3 in configs: + name = l3.get('name') + ipv4_addrs = [] + ipv4_anycast = [] + if l3.get('ipv4'): + if l3['ipv4'].get('addresses'): + ipv4_addrs = l3['ipv4']['addresses'] + if l3['ipv4'].get('anycast_addresses', None): + ipv4_anycast = l3['ipv4']['anycast_addresses'] + + ipv6_addrs = [] + ipv6_enabled = None + if l3.get('ipv6'): + if l3['ipv6'].get('addresses'): + ipv6_addrs = l3['ipv6']['addresses'] + if 'enabled' in l3['ipv6']: + ipv6_enabled = l3['ipv6']['enabled'] + + sub_intf = self.get_sub_interface_name(name) + + if ipv4_addrs: + ipv4_addrs_delete_request = {"path": ipv4_addrs_url_all.format(intf_name=name, sub_intf_name=sub_intf), "method": DELETE} + requests.append(ipv4_addrs_delete_request) + if ipv4_anycast: + for ip in ipv4_anycast: + ip = ip.replace('/', '%2f') + anycast_delete_request = {"path": ipv4_anycast_url.format(intf_name=name, sub_intf_name=sub_intf, anycast_ip=ip), "method": DELETE} + requests.append(anycast_delete_request) + if ipv6_addrs: + ipv6_addrs_delete_request = {"path": ipv6_addrs_url_all.format(intf_name=name, sub_intf_name=sub_intf), "method": DELETE} + requests.append(ipv6_addrs_delete_request) + if ipv6_enabled: + ipv6_enabled_delete_request = {"path": ipv6_enabled_url.format(intf_name=name, sub_intf_name=sub_intf), "method": DELETE} + requests.append(ipv6_enabled_delete_request) + return requests + + def get_create_l3_interfaces_requests(self, configs, have, want): + requests = [] + if not configs: + return requests + + ipv4_addrs_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv4/addresses' + ipv4_anycast_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv4/' + ipv4_anycast_url += 'openconfig-interfaces-ext:sag-ipv4/config/static-anycast-gateway' + ipv6_addrs_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv6/addresses' + ipv6_enabled_url = 'data/openconfig-interfaces:interfaces/interface={intf_name}/{sub_intf_name}/openconfig-if-ip:ipv6/config' + + for l3 in configs: + l3_interface_name = l3.get('name') + if l3_interface_name == "eth0": + continue + + sub_intf = self.get_sub_interface_name(l3_interface_name) + + ipv4_addrs = [] + ipv4_anycast = [] + if l3.get('ipv4'): + if l3['ipv4'].get('addresses'): + ipv4_addrs = l3['ipv4']['addresses'] + if l3['ipv4'].get('anycast_addresses'): + ipv4_anycast = l3['ipv4']['anycast_addresses'] + + ipv6_addrs = [] + ipv6_enabled = None + if l3.get('ipv6'): + if l3['ipv6'].get('addresses'): + ipv6_addrs = l3['ipv6']['addresses'] + if 'enabled' in l3['ipv6']: + ipv6_enabled = l3['ipv6']['enabled'] + + if ipv4_addrs: + ipv4_addrs_pri_payload = [] + ipv4_addrs_sec_payload = [] + for item in ipv4_addrs: + ipv4_addr_mask = item['address'].split('/') + ipv4 = ipv4_addr_mask[0] + ipv4_mask = ipv4_addr_mask[1] + ipv4_secondary = item['secondary'] + if ipv4_secondary: + ipv4_addrs_sec_payload.append(self.build_create_addr_payload(ipv4, ipv4_mask, ipv4_secondary)) + else: + ipv4_addrs_pri_payload.append(self.build_create_addr_payload(ipv4, ipv4_mask, ipv4_secondary)) + if ipv4_addrs_pri_payload: + payload = self.build_create_payload(ipv4_addrs_pri_payload) + ipv4_addrs_req = {"path": ipv4_addrs_url.format(intf_name=l3_interface_name, sub_intf_name=sub_intf), "method": PATCH, "data": payload} + requests.append(ipv4_addrs_req) + if ipv4_addrs_sec_payload: + payload = self.build_create_payload(ipv4_addrs_sec_payload) + ipv4_addrs_req = {"path": ipv4_addrs_url.format(intf_name=l3_interface_name, sub_intf_name=sub_intf), "method": PATCH, "data": payload} + requests.append(ipv4_addrs_req) + + if ipv4_anycast: + anycast_payload = {'openconfig-interfaces-ext:static-anycast-gateway': ipv4_anycast} + anycast_url = ipv4_anycast_url.format(intf_name=l3_interface_name, sub_intf_name=sub_intf) + requests.append({'path': anycast_url, 'method': PATCH, 'data': anycast_payload}) + + if ipv6_addrs: + ipv6_addrs_payload = [] + for item in ipv6_addrs: + ipv6_addr_mask = item['address'].split('/') + ipv6 = ipv6_addr_mask[0] + ipv6_mask = ipv6_addr_mask[1] + ipv6_addrs_payload.append(self.build_create_addr_payload(ipv6, ipv6_mask)) + if ipv6_addrs_payload: + payload = self.build_create_payload(ipv6_addrs_payload) + ipv6_addrs_req = {"path": ipv6_addrs_url.format(intf_name=l3_interface_name, sub_intf_name=sub_intf), "method": PATCH, "data": payload} + requests.append(ipv6_addrs_req) + + if ipv6_enabled is not None: + payload = self.build_update_ipv6_enabled(ipv6_enabled) + ipv6_enabled_req = {"path": ipv6_enabled_url.format(intf_name=l3_interface_name, sub_intf_name=sub_intf), "method": PATCH, "data": payload} + requests.append(ipv6_enabled_req) + + return requests + + def validate_primary_ips(self, want): + error_intf = {} + for l3 in want: + l3_interface_name = l3.get('name') + + ipv4_addrs = [] + if l3.get('ipv4') and l3['ipv4'].get('addresses'): + ipv4_addrs = l3['ipv4']['addresses'] + + if ipv4_addrs: + ipv4_pri_addrs = [addr['address'] for addr in ipv4_addrs if not addr['secondary']] + if len(ipv4_pri_addrs) > 1: + error_intf[l3_interface_name] = ipv4_pri_addrs + + if error_intf: + err = "Multiple ipv4 primary ips found! " + str(error_intf) + self._module.fail_json(msg=str(err), code=300) + + def build_create_payload(self, addrs_payload): + payload = {'openconfig-if-ip:addresses': {'address': addrs_payload}} + return payload + + def build_create_addr_payload(self, ip, mask, secondary=None): + cfg = {'ip': ip, 'prefix-length': float(mask)} + if secondary: + cfg['secondary'] = secondary + addr_payload = {'ip': ip, 'openconfig-if-ip:config': cfg} + return addr_payload + + def get_sub_interface_name(self, name): + sub_intf = "subinterfaces/subinterface=0" + if name.startswith("Vlan"): + sub_intf = "openconfig-vlan:routed-vlan" + return sub_intf + + def build_update_ipv6_enabled(self, ipv6_enabled): + payload = {'config': {'enabled': ipv6_enabled}} + return payload diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lag_interfaces/lag_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lag_interfaces/lag_interfaces.py new file mode 100644 index 000000000..541de2c4c --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lag_interfaces/lag_interfaces.py @@ -0,0 +1,421 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_lag_interfaces 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 + +try: + from urllib import quote +except ImportError: + from urllib.parse import quote + +import json + +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, + search_obj_in_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, + normalize_interface_name, + remove_empties_from_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.sonic import ( + to_request, + edit_config +) +from ansible.module_utils._text import to_native +from ansible.module_utils.connection import ConnectionError +import traceback + +LIB_IMP_ERR = None +ERR_MSG = None +try: + import jinja2 + HAS_LIB = True +except Exception as e: + HAS_LIB = False + ERR_MSG = to_native(e) + LIB_IMP_ERR = traceback.format_exc() + + +PUT = 'put' +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'interfaces': {'member': ''}}, +] + + +class Lag_interfaces(ConfigBase): + """ + The sonic_lag_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'lag_interfaces', + ] + + params = ('name', 'members') + + def __init__(self, module): + super(Lag_interfaces, self).__init__(module) + + def get_lag_interfaces_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) + lag_interfaces_facts = facts['ansible_network_resources'].get('lag_interfaces') + if not lag_interfaces_facts: + return [] + return lag_interfaces_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_lag_interfaces_facts = self.get_lag_interfaces_facts() + commands, requests = self.set_config(existing_lag_interfaces_facts) + if commands: + 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_lag_interfaces_facts = self.get_lag_interfaces_facts() + + result['before'] = existing_lag_interfaces_facts + if result['changed']: + result['after'] = changed_lag_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_lag_interfaces_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'] + normalize_interface_name(want, self._module) + have = existing_lag_interfaces_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 + """ + commands = [] + diff = get_diff(want, have, TEST_KEYS) + if diff: + diff_members, diff_portchannels = self.diff_list_for_member_creation(diff) + else: + diff_members = [] + diff_portchannels = [] + + state = self._module.params['state'] + if state in ('overridden', 'merged', 'replaced') and not want: + self._module.fail_json(msg='value of config parameter must not be empty for state {0}'.format(state)) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff_members, diff_portchannels) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff_members, diff_portchannels) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff_members, diff_portchannels) + + return commands, requests + + def _state_replaced(self, want, have, diff_members, diff_portchannels): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + requests = list() + commands = list() + delete_list = list() + delete_list = get_diff(have, want, TEST_KEYS) + delete_members, delete_portchannels = self.diff_list_for_member_creation(delete_list) + replaced_list = list() + + for i in want: + list_obj = search_obj_in_list(i['name'], delete_members, "name") + if list_obj: + replaced_list.append(list_obj) + requests = self.get_delete_lag_interfaces_requests(replaced_list) + if requests: + commands.extend(update_states(replaced_list, "replaced")) + replaced_commands, replaced_requests = self.template_for_lag_creation(have, diff_members, diff_portchannels, "replaced") + if replaced_requests: + commands.extend(replaced_commands) + requests.extend(replaced_requests) + + return commands, requests + + def _state_overridden(self, want, have, diff_members, diff_portchannels): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + requests = list() + commands = list() + delete_list = list() + delete_list = get_diff(have, want, TEST_KEYS) + delete_members, delete_portchannels = self.diff_list_for_member_creation(delete_list) + replaced_list = list() + for i in want: + list_obj = search_obj_in_list(i['name'], delete_members, "name") + if list_obj: + replaced_list.append(list_obj) + requests = self.get_delete_lag_interfaces_requests(replaced_list) + commands.extend(update_states(replaced_list, "overridden")) + delete_members = get_diff(delete_members, replaced_list, TEST_KEYS) + commands_overridden, requests_overridden = self.template_for_lag_deletion(have, delete_members, delete_portchannels, "overridden") + requests.extend(requests_overridden) + commands.extend(commands_overridden) + override_commands, override_requests = self.template_for_lag_creation(have, diff_members, diff_portchannels, "overridden") + commands.extend(override_commands) + requests.extend(override_requests) + return commands, requests + + def _state_merged(self, want, have, diff_members, diff_portchannels): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + return self.template_for_lag_creation(have, diff_members, diff_portchannels, "merged") + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = list() + requests = list() + portchannel_requests = list() + # if want is none, then delete all the lag interfaces and all portchannels + if not want: + requests = self.get_delete_all_lag_interfaces_requests() + portchannel_requests = self.get_delete_all_portchannel_requests() + requests.extend(portchannel_requests) + commands.extend(update_states(have, "Deleted")) + else: # delete specific lag interfaces and specific portchannels + commands = get_diff(want, diff, TEST_KEYS) + commands = remove_empties_from_list(commands) + want_members, want_portchannels = self.diff_list_for_member_creation(commands) + commands, requests = self.template_for_lag_deletion(have, want_members, want_portchannels, "deleted") + return commands, requests + + def diff_list_for_member_creation(self, diff): + diff_members = [x for x in diff if "members" in x.keys()] + diff_portchannels = [x for x in diff if ("name" in x.keys() and "members" not in x.keys())] + return diff_members, diff_portchannels + + def template_for_lag_creation(self, have, diff_members, diff_portchannels, state_name): + commands = list() + requests = list() + if diff_members: + commands_portchannels, requests = self.call_create_port_channel(diff_members, have) + if commands_portchannels: + po_list = [{'name': x['name']} for x in commands_portchannels if x['name']] + else: + po_list = [] + if po_list: + commands.extend(update_states(po_list, state_name)) + diff_members_remove_none = [x for x in diff_members if x["members"]] + if diff_members_remove_none: + request = self.create_lag_interfaces_requests(diff_members_remove_none) + if request: + requests.extend(request) + else: + requests = request + commands.extend(update_states(diff_members, state_name)) + if diff_portchannels: + portchannels, po_requests = self.call_create_port_channel(diff_portchannels, have) + requests.extend(po_requests) + commands.extend(update_states(portchannels, state_name)) + return commands, requests + + def template_for_lag_deletion(self, have, delete_members, delete_portchannels, state_name): + commands = list() + requests = list() + portchannel_requests = list() + if delete_members: + delete_members_remove_none = [x for x in delete_members if x["members"]] + requests = self.get_delete_lag_interfaces_requests(delete_members_remove_none) + delete_all_members = [x for x in delete_members if "members" in x.keys() and not x["members"]] + delete_all_list = list() + if delete_all_members: + for i in delete_all_members: + list_obj = search_obj_in_list(i['name'], have, "name") + if list_obj['members']: + delete_all_list.append(list_obj) + if delete_all_list: + deleteall_requests = self.get_delete_lag_interfaces_requests(delete_all_list) + else: + deleteall_requests = [] + if requests and deleteall_requests: + requests.extend(deleteall_requests) + elif deleteall_requests: + requests = deleteall_requests + if requests: + commands.extend(update_states(delete_members, state_name)) + if delete_portchannels: + portchannel_requests = self.get_delete_portchannel_requests(delete_portchannels) + commands.extend(update_states(delete_portchannels, state_name)) + if requests: + requests.extend(portchannel_requests) + else: + requests = portchannel_requests + return commands, requests + + def create_lag_interfaces_requests(self, commands): + requests = [] + for i in commands: + if i.get('members') and i['members'].get('interfaces'): + interfaces = i['members']['interfaces'] + else: + continue + for each in interfaces: + edit_payload = self.build_create_payload_member(i['name']) + template = 'data/openconfig-interfaces:interfaces/interface=%s/openconfig-if-ethernet:ethernet/config/openconfig-if-aggregate:aggregate-id' + edit_path = template % quote(each['member'], safe='') + request = {'path': edit_path, 'method': PATCH, 'data': edit_payload} + requests.append(request) + return requests + + def build_create_payload_member(self, name): + payload_template = """{\n"openconfig-if-aggregate:aggregate-id": "{{name}}"\n}""" + temp = name.split("PortChannel", 1)[1] + input_data = {"name": temp} + env = jinja2.Environment(autoescape=False) + t = env.from_string(payload_template) + intended_payload = t.render(input_data) + ret_payload = json.loads(intended_payload) + return ret_payload + + def build_create_payload_portchannel(self, name, mode): + payload_template = """{\n"openconfig-interfaces:interfaces": {"interface": [{\n"name": "{{name}}",\n"config": {\n"name": "{{name}}"\n}""" + input_data = {"name": name} + if mode == "static": + payload_template += """,\n "openconfig-if-aggregation:aggregation": {\n"config": {\n"lag-type": "{{mode}}"\n}\n}\n""" + input_data["mode"] = mode.upper() + payload_template += """}\n]\n}\n}""" + env = jinja2.Environment(autoescape=False) + t = env.from_string(payload_template) + intended_payload = t.render(input_data) + ret_payload = json.loads(intended_payload) + return ret_payload + + def create_port_channel(self, cmd): + requests = [] + path = 'data/openconfig-interfaces:interfaces' + for i in cmd: + payload = self.build_create_payload_portchannel(i['name'], i.get('mode', None)) + request = {'path': path, 'method': PATCH, 'data': payload} + requests.append(request) + return requests + + def call_create_port_channel(self, commands, have): + commands_list = list() + for c in commands: + if not any(d['name'] == c['name'] for d in have): + commands_list.append(c) + requests = self.create_port_channel(commands_list) + return commands_list, requests + + def get_delete_all_lag_interfaces_requests(self): + requests = [] + delete_all_lag_url = 'data/sonic-portchannel:sonic-portchannel/PORTCHANNEL_MEMBER/PORTCHANNEL_MEMBER_LIST' + method = DELETE + delete_all_lag_request = {"path": delete_all_lag_url, "method": method} + requests.append(delete_all_lag_request) + return requests + + def get_delete_all_portchannel_requests(self): + requests = [] + delete_all_lag_url = 'data/sonic-portchannel:sonic-portchannel/PORTCHANNEL/PORTCHANNEL_LIST' + method = DELETE + delete_all_lag_request = {"path": delete_all_lag_url, "method": method} + requests.append(delete_all_lag_request) + return requests + + def get_delete_lag_interfaces_requests(self, commands): + requests = [] + # Create URL and payload + url = 'data/openconfig-interfaces:interfaces/interface={}/openconfig-if-ethernet:ethernet/config/openconfig-if-aggregate:aggregate-id' + method = DELETE + for c in commands: + if c.get('members') and c['members'].get('interfaces'): + interfaces = c['members']['interfaces'] + else: + continue + + for each in interfaces: + ifname = each["member"] + request = {"path": url.format(ifname), "method": method} + requests.append(request) + + return requests + + def get_delete_portchannel_requests(self, commands): + requests = [] + # Create URL and payload + url = 'data/openconfig-interfaces:interfaces/interface={}' + method = DELETE + for c in commands: + name = c["name"] + request = {"path": url.format(name), "method": method} + requests.append(request) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mclag/mclag.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mclag/mclag.py new file mode 100644 index 000000000..88215e8fc --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mclag/mclag.py @@ -0,0 +1,323 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_mclag 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 ( + update_states, + get_diff, + get_normalize_interface_name, + normalize_interface_name +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'delete' + +TEST_KEYS = [ + {'config': {'domain_id': ''}}, + {'vlans': {'vlan': ''}}, + {'portchannels': {'lag': ''}}, +] + + +class Mclag(ConfigBase): + """ + The sonic_mclag class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'mclag', + ] + + def __init__(self, module): + super(Mclag, self).__init__(module) + + def get_mclag_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) + mclag_facts = facts['ansible_network_resources'].get('mclag') + if not mclag_facts: + return [] + return mclag_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_mclag_facts = self.get_mclag_facts() + commands, requests = self.set_config(existing_mclag_facts) + if commands and len(requests) > 0: + if not self._module.check_mode: + self.edit_config(requests) + result['changed'] = True + result['commands'] = commands + + changed_mclag_facts = self.get_mclag_facts() + + result['before'] = existing_mclag_facts + if result['changed']: + result['after'] = changed_mclag_facts + + result['warnings'] = warnings + return result + + def edit_config(self, requests): + try: + response = edit_config(self._module, to_request(self._module, requests)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + def set_config(self, existing_mclag_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'] + if want: + peer_link = want.get("peer_link", None) + if peer_link: + want['peer_link'] = get_normalize_interface_name(want['peer_link'], self._module) + unique_ip = want.get('unique_ip', None) + if unique_ip: + vlans_list = unique_ip['vlans'] + if vlans_list: + normalize_interface_name(vlans_list, self._module, 'vlan') + members = want.get('members', None) + if members: + portchannels_list = members['portchannels'] + if portchannels_list: + normalize_interface_name(portchannels_list, self._module, 'lag') + have = existing_mclag_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 == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged': + diff = get_diff(want, have, TEST_KEYS) + commands = self._state_merged(want, have, diff) + return commands + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + requests = [] + commands = [] + if diff: + requests = self.get_create_mclag_request(want, diff) + if len(requests) > 0: + commands = update_states(diff, "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 = [] + if not want: + if have: + requests = self.get_delete_all_mclag_domain_request() + if len(requests) > 0: + commands = update_states(have, "deleted") + else: + new_have = self.remove_default_entries(have) + d_diff = get_diff(want, new_have, TEST_KEYS, is_skeleton=True) + diff_want = get_diff(want, d_diff, TEST_KEYS, is_skeleton=True) + if diff_want: + requests = self.get_delete_mclag_attribute_request(want, diff_want) + if len(requests) > 0: + commands = update_states(diff_want, "deleted") + return commands, requests + + def remove_default_entries(self, data): + new_data = {} + if not data: + return new_data + else: + default_val_dict = { + 'keepalive': 1, + 'session_timeout': 30, + } + for key, val in data.items(): + if not (val is None or (key in default_val_dict and val == default_val_dict[key])): + new_data[key] = val + + return new_data + + def get_delete_mclag_attribute_request(self, want, command): + requests = [] + url_common = 'data/openconfig-mclag:mclag/mclag-domains/mclag-domain=%s/config' % (want["domain_id"]) + method = DELETE + if 'source_address' in command and command["source_address"] is not None: + url = url_common + '/source-address' + request = {'path': url, 'method': method} + requests.append(request) + if 'peer_address' in command and command["peer_address"] is not None: + url = url_common + '/peer-address' + request = {'path': url, 'method': method} + requests.append(request) + if 'peer_link' in command and command["peer_link"] is not None: + url = url_common + '/peer-link' + request = {'path': url, 'method': method} + requests.append(request) + if 'keepalive' in command and command["keepalive"] is not None: + url = url_common + '/keepalive-interval' + request = {'path': url, 'method': method} + requests.append(request) + if 'session_timeout' in command and command["session_timeout"] is not None: + url = url_common + '/session-timeout' + request = {'path': url, 'method': method} + requests.append(request) + if 'system_mac' in command and command["system_mac"] is not None: + url = url_common + '/mclag-system-mac' + request = {'path': url, 'method': method} + requests.append(request) + if 'unique_ip' in command and command['unique_ip'] is not None: + if command['unique_ip']['vlans'] is None: + request = {'path': 'data/openconfig-mclag:mclag/vlan-interfaces/vlan-interface', 'method': method} + requests.append(request) + elif command['unique_ip']['vlans'] is not None: + for each in command['unique_ip']['vlans']: + if each: + unique_ip_url = 'data/openconfig-mclag:mclag/vlan-interfaces/vlan-interface=%s' % (each['vlan']) + request = {'path': unique_ip_url, 'method': method} + requests.append(request) + if 'members' in command and command['members'] is not None: + if command['members']['portchannels'] is None: + request = {'path': 'data/openconfig-mclag:mclag/interfaces/interface', 'method': method} + requests.append(request) + elif command['members']['portchannels'] is not None: + for each in command['members']['portchannels']: + if each: + portchannel_url = 'data/openconfig-mclag:mclag/interfaces/interface=%s' % (each['lag']) + request = {'path': portchannel_url, 'method': method} + requests.append(request) + return requests + + def get_delete_all_mclag_domain_request(self): + requests = [] + path = 'data/openconfig-mclag:mclag/mclag-domains' + method = DELETE + request = {'path': path, 'method': method} + requests.append(request) + return requests + + def get_create_mclag_request(self, want, commands): + requests = [] + path = 'data/openconfig-mclag:mclag/mclag-domains/mclag-domain' + method = PATCH + payload = self.build_create_payload(want, commands) + if payload: + request = {'path': path, 'method': method, 'data': payload} + requests.append(request) + if 'unique_ip' in commands and commands['unique_ip'] is not None: + if commands['unique_ip']['vlans'] and commands['unique_ip']['vlans'] is not None: + unique_ip_path = 'data/openconfig-mclag:mclag/vlan-interfaces/vlan-interface' + unique_ip_method = PATCH + unique_ip_payload = self.build_create_unique_ip_payload(commands['unique_ip']['vlans']) + request = {'path': unique_ip_path, 'method': unique_ip_method, 'data': unique_ip_payload} + requests.append(request) + if 'members' in commands and commands['members'] is not None: + if commands['members']['portchannels'] and commands['members']['portchannels'] is not None: + portchannel_path = 'data/openconfig-mclag:mclag/interfaces/interface' + portchannel_method = PATCH + portchannel_payload = self.build_create_portchannel_payload(want, commands['members']['portchannels']) + request = {'path': portchannel_path, 'method': portchannel_method, 'data': portchannel_payload} + requests.append(request) + return requests + + def build_create_payload(self, want, commands): + temp = {} + if 'session_timeout' in commands and commands['session_timeout'] is not None: + temp['session-timeout'] = commands['session_timeout'] + if 'keepalive' in commands and commands['keepalive'] is not None: + temp['keepalive-interval'] = commands['keepalive'] + if 'source_address' in commands and commands['source_address'] is not None: + temp['source-address'] = commands['source_address'] + if 'peer_address' in commands and commands['peer_address'] is not None: + temp['peer-address'] = commands['peer_address'] + if 'peer_link' in commands and commands['peer_link'] is not None: + temp['peer-link'] = str(commands['peer_link']) + if 'system_mac' in commands and commands['system_mac'] is not None: + temp['openconfig-mclag:mclag-system-mac'] = str(commands['system_mac']) + mclag_dict = {} + if temp: + domain_id = {"domain-id": want["domain_id"]} + mclag_dict.update(domain_id) + config = {"config": temp} + mclag_dict.update(config) + payload = {"openconfig-mclag:mclag-domain": [mclag_dict]} + else: + payload = {} + return payload + + def build_create_unique_ip_payload(self, commands): + payload = {"openconfig-mclag:vlan-interface": []} + for each in commands: + payload['openconfig-mclag:vlan-interface'].append({"name": each['vlan'], "config": {"name": each['vlan'], "unique-ip-enable": "ENABLE"}}) + return payload + + def build_create_portchannel_payload(self, want, commands): + payload = {"openconfig-mclag:interface": []} + for each in commands: + payload['openconfig-mclag:interface'].append({"name": each['lag'], "config": {"name": each['lag'], "mclag-domain-id": want['domain_id']}}) + return payload diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/ntp/ntp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/ntp/ntp.py new file mode 100644 index 000000000..a4fdc7e0a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/ntp/ntp.py @@ -0,0 +1,548 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 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_ntp 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 + +import re + +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states, + normalize_interface_name, + normalize_interface_name_list +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'PATCH' +DELETE = 'DELETE' + +TEST_KEYS = [ + { + "vrf": "", "enable_ntp_auth": "", "source_interfaces": "", "trusted_keys": "", + "servers": {"address": ""}, "ntp_keys": {"key_id": ""} + } +] + + +class Ntp(ConfigBase): + """ + The sonic_ntp class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'ntp', + ] + + def __init__(self, module): + super(Ntp, self).__init__(module) + + def get_ntp_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) + ntp_facts = facts['ansible_network_resources'].get('ntp') + + if not ntp_facts: + return [] + return ntp_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + commands = list() + requests = list() + + existing_ntp_facts = self.get_ntp_facts() + + commands, requests = self.set_config(existing_ntp_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_ntp_facts = self.get_ntp_facts() + + result['before'] = existing_ntp_facts + if result['changed']: + result['after'] = changed_ntp_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_ntp_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'] + if want is None: + want = [] + + have = existing_ntp_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'] + + self.validate_want(want, state) + self.preprocess_want(want, state) + + if state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(want, have) + + return commands, requests + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param have: the current configuration as a dictionary + :returns: the commands necessary to merge the provided into + the current configuration + """ + diff = get_diff(want, have, TEST_KEYS) + + commands = diff + requests = [] + if commands: + requests = self.get_merge_requests(commands, have) + + if len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param have: the current configuration as a dictionary + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + diff = get_diff(want, have, TEST_KEYS) + + want_none = {'enable_ntp_auth': None, 'ntp_keys': None, + 'servers': None, 'source_interfaces': [], + 'trusted_keys': None, 'vrf': None} + want_any = get_diff(want, want_none, TEST_KEYS) + # if want_any is none, then delete all NTP configurations + + delete_all = False + if not want_any: + commands = have + delete_all = True + else: + if not diff: + commands = want_any + else: + commands = get_diff(want_any, diff, TEST_KEYS) + + requests = [] + if commands: + requests = self.get_delete_requests(commands, delete_all) + + if len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def validate_want(self, want, state): + + if state == 'deleted': + if 'servers' in want and want['servers'] is not None: + for server in want['servers']: + key_id_config = server.get('key_id', None) + minpoll_config = server.get('minpoll', None) + maxpoll_config = server.get('maxpoll', None) + if key_id_config or minpoll_config or maxpoll_config: + err_msg = "NTP server parameter(s) can not be deleted." + self._module.fail_json(msg=err_msg, code=405) + + if 'ntp_keys' in want and want['ntp_keys'] is not None: + for ntp_key in want['ntp_keys']: + encrypted_config = ntp_key.get('encrypted', None) + key_type_config = ntp_key.get('key_type', None) + key_value_config = ntp_key.get('key_value', None) + if encrypted_config or key_type_config or key_value_config: + err_msg = "NTP ntp_key parameter(s) can not be deleted." + self._module.fail_json(msg=err_msg, code=405) + + def preprocess_want(self, want, state): + + if 'source_interfaces' in want: + want['source_interfaces'] = normalize_interface_name_list(want['source_interfaces'], self._module) + + if state == 'deleted': + enable_auth_want = want.get('enable_ntp_auth', None) + if enable_auth_want is not None: + want['enable_ntp_auth'] = True + + elif state == 'merged': + if 'servers' in want and want['servers'] is not None: + for server in want['servers']: + if 'key_id' in server and not server['key_id']: + server.pop('key_id') + if 'minpoll' in server and not server['minpoll']: + server.pop('minpoll') + if 'maxpoll' in server and not server['maxpoll']: + server.pop('maxpoll') + + def get_merge_requests(self, configs, have): + + requests = [] + + enable_auth_config = configs.get('enable_ntp_auth', None) + if enable_auth_config is not None: + enable_auth_request = self.get_create_enable_ntp_auth_requests(enable_auth_config, have) + if enable_auth_request: + requests.extend(enable_auth_request) + + src_intf_config = configs.get('source_interfaces', None) + if src_intf_config: + src_intf_request = self.get_create_source_interface_requests(src_intf_config, have) + if src_intf_request: + requests.extend(src_intf_request) + + keys_config = configs.get('ntp_keys', None) + if keys_config: + keys_request = self.get_create_keys_requests(keys_config, have) + if keys_request: + requests.extend(keys_request) + + servers_config = configs.get('servers', None) + if servers_config: + servers_request = self.get_create_servers_requests(servers_config, have) + if servers_request: + requests.extend(servers_request) + + trusted_key_config = configs.get('trusted_keys', None) + if trusted_key_config: + trusted_key_request = self.get_create_trusted_key_requests(trusted_key_config, have) + if trusted_key_request: + requests.extend(trusted_key_request) + + vrf_config = configs.get('vrf', None) + if vrf_config: + vrf_request = self.get_create_vrf_requests(vrf_config, have) + if vrf_request: + requests.extend(vrf_request) + + return requests + + def get_delete_requests(self, configs, delete_all): + + requests = [] + + if delete_all: + all_ntp_request = self.get_delete_all_ntp_requests(configs) + if all_ntp_request: + requests.extend(all_ntp_request) + return requests + + src_intf_config = configs.get('source_interfaces', None) + if src_intf_config: + src_intf_request = self.get_delete_source_interface_requests(src_intf_config) + if src_intf_request: + requests.extend(src_intf_request) + + servers_config = configs.get('servers', None) + if servers_config: + servers_request = self.get_delete_servers_requests(servers_config) + if servers_request: + requests.extend(servers_request) + + trusted_key_config = configs.get('trusted_keys', None) + if trusted_key_config: + trusted_key_request = self.get_delete_trusted_key_requests(trusted_key_config) + if trusted_key_request: + requests.extend(trusted_key_request) + + keys_config = configs.get('ntp_keys', None) + if keys_config: + keys_request = self.get_delete_keys_requests(keys_config) + if keys_request: + requests.extend(keys_request) + + enable_auth_config = configs.get('enable_ntp_auth', None) + if enable_auth_config is not None: + enable_auth_request = self.get_delete_enable_ntp_auth_requests(enable_auth_config) + if enable_auth_request: + requests.extend(enable_auth_request) + + vrf_config = configs.get('vrf', None) + if vrf_config: + vrf_request = self.get_delete_vrf_requests(vrf_config) + if vrf_request: + requests.extend(vrf_request) + + return requests + + def get_create_source_interface_requests(self, configs, have): + + requests = [] + + # Create URL and payload + method = PATCH + url = 'data/openconfig-system:system/ntp/config/source-interface' + payload = {"openconfig-system:source-interface": configs} + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_create_servers_requests(self, configs, have): + + requests = [] + + # Create URL and payload + method = PATCH + url = 'data/openconfig-system:system/ntp/servers' + server_configs = [] + for config in configs: + if 'key_id' in config: + config['key-id'] = config['key_id'] + config.pop('key_id') + server_addr = config['address'] + server_config = {"address": server_addr, "config": config} + server_configs.append(server_config) + + payload = {"openconfig-system:servers": {"server": server_configs}} + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_create_vrf_requests(self, configs, have): + + requests = [] + + # Create URL and payload + method = PATCH + url = 'data/openconfig-system:system/ntp/config/network-instance' + payload = {"openconfig-system:network-instance": configs} + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_create_enable_ntp_auth_requests(self, configs, have): + + requests = [] + + # Create URL and payload + method = PATCH + url = 'data/openconfig-system:system/ntp/config/enable-ntp-auth' + payload = {"openconfig-system:enable-ntp-auth": configs} + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_create_trusted_key_requests(self, configs, have): + + requests = [] + + # Create URL and payload + method = PATCH + url = 'data/openconfig-system:system/ntp/config/trusted-key' + payload = {"openconfig-system:trusted-key": configs} + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_create_keys_requests(self, configs, have): + + requests = [] + + # Create URL and payload + method = PATCH + url = 'data/openconfig-system:system/ntp/ntp-keys' + key_configs = [] + for config in configs: + key_id = config['key_id'] + if 'key_id' in config: + config['key-id'] = config['key_id'] + config.pop('key_id') + if 'key_type' in config: + config['key-type'] = config['key_type'] + config.pop('key_type') + if 'key_value' in config: + config['key-value'] = config['key_value'] + config.pop('key_value') + + key_config = {"key-id": key_id, "config": config} + key_configs.append(key_config) + + payload = {"openconfig-system:ntp-keys": {"ntp-key": key_configs}} + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_delete_all_ntp_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + + servers_config = configs.get('servers', None) + src_intf_config = configs.get('source_interfaces', None) + vrf_config = configs.get('vrf', None) + enable_auth_config = configs.get('enable_ntp_auth', None) + trusted_key_config = configs.get('trusted_keys', None) + + if servers_config or src_intf_config or vrf_config or \ + trusted_key_config or enable_auth_config is not None: + url = 'data/openconfig-system:system/ntp' + request = {"path": url, "method": method} + requests.append(request) + + keys_config = configs.get('ntp_keys', None) + if keys_config: + url = 'data/openconfig-system:system/ntp/ntp-keys' + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_delete_source_interface_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + for config in configs: + url = 'data/openconfig-system:system/ntp/config/source-interface={0}'.format(config) + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_delete_servers_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + for config in configs: + server_addr = config['address'] + url = 'data/openconfig-system:system/ntp/servers/server={0}'.format(server_addr) + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_delete_vrf_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + url = 'data/openconfig-system:system/ntp/config/network-instance' + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_delete_enable_ntp_auth_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + url = 'data/openconfig-system:system/ntp/config/enable-ntp-auth' + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_delete_trusted_key_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + for config in configs: + url = 'data/openconfig-system:system/ntp/config/trusted-key={0}'.format(config) + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_delete_keys_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + key_configs = [] + for config in configs: + key_id = config['key_id'] + url = 'data/openconfig-system:system/ntp/ntp-keys/ntp-key={0}'.format(key_id) + request = {"path": url, "method": method} + requests.append(request) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/port_breakout/port_breakout.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/port_breakout/port_breakout.py new file mode 100644 index 000000000..371019d04 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/port_breakout/port_breakout.py @@ -0,0 +1,260 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_port_breakout 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.module_utils.connection import ConnectionError +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, + get_speed_from_breakout_mode, + get_breakout_mode, +) + +PATCH = 'patch' +DELETE = 'delete' +POST = 'post' + + +class Port_breakout(ConfigBase): + """ + The sonic_port_breakout class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'port_breakout', + ] + + def __init__(self, module): + super(Port_breakout, self).__init__(module) + + def get_port_breakout_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) + port_breakout_facts = facts['ansible_network_resources'].get('port_breakout') + if not port_breakout_facts: + return [] + return port_breakout_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_port_breakout_facts = self.get_port_breakout_facts() + commands, requests = self.set_config(existing_port_breakout_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_port_breakout_facts = self.get_port_breakout_facts() + + result['before'] = existing_port_breakout_facts + if result['changed']: + result['after'] = changed_port_breakout_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_port_breakout_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_port_breakout_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + if not want: + want = [] + + have_new = self.get_all_breakout_mode(have) + diff = get_diff(want, have_new) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_port_breakout_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the port_breakouti except admin + if not want: + commands = have + else: + commands = want + + requests = self.get_delete_port_breakout_requests(commands, have) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_port_breakout_payload(self, name, mode, match): + payload = {} + speed = get_speed_from_breakout_mode(mode) + if speed: + num_breakouts = int(mode[0]) + mode_cfg = {'groups': {'group': [{'index': 1, 'config': {'index': 1, 'num-breakouts': num_breakouts, 'breakout-speed': speed}}]}} + port_cfg = {'openconfig-platform-port:breakout-mode': mode_cfg} + compo_cfg = {'name': name, 'port': port_cfg} + payload = {'openconfig-platform:components': {'component': [compo_cfg]}} + return payload + + def get_delete_single_port_breakout(self, name, match): + del_req = None + if match: + del_url = 'data/openconfig-platform:components/component=%s/port/openconfig-platform-port:breakout-mode' % (name.replace('/', '%2f')) + del_req = {'path': del_url, 'method': DELETE} + return del_req + + def get_modify_port_breakout_request(self, conf, match): + request = None + name = conf.get('name', None) + mode = conf.get('mode', None) + url = 'data/openconfig-platform:components' + payload = self.get_port_breakout_payload(name, mode, match) + request = {'path': url, 'method': PATCH, 'data': payload} + return request + + def get_modify_port_breakout_requests(self, commands, have): + requests = [] + if not commands: + return requests + + for conf in commands: + match = next((cfg for cfg in have if cfg['name'] == conf['name']), None) + req = self.get_modify_port_breakout_request(conf, match) + if req: + requests.append(req) + return requests + + def get_default_port_breakout_modes(self): + def_port_breakout_modes = [] + request = [{"path": "operations/sonic-port-breakout:breakout_capabilities", "method": POST}] + 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) + + raw_port_breakout_list = [] + if "sonic-port-breakout:output" in response[0][1]: + raw_port_breakout_list = response[0][1].get("sonic-port-breakout:output", {}).get('caps', []) + + for port_breakout in raw_port_breakout_list: + name = port_breakout.get('port', None) + mode = port_breakout.get('defmode', None) + if name and mode: + if '[' in mode: + mode = mode[:mode.index('[')] + def_port_breakout_modes.append({'name': name, 'mode': mode}) + return def_port_breakout_modes + + def get_delete_port_breakout_requests(self, commands, have): + requests = [] + if not commands: + return requests + + have_new = self.get_all_breakout_mode(have) + for conf in commands: + name = conf['name'] + match = next((cfg for cfg in have_new if cfg['name'] == name), None) + req = self.get_delete_single_port_breakout(name, match) + if req: + requests.append(req) + return requests + + def get_all_breakout_mode(self, have): + new_have = [] + for cfg in have: + name = cfg['name'] + mode = get_breakout_mode(self._module, name) + if mode: + new_have.append({'name': name, 'mode': mode}) + return new_have diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/prefix_lists/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/prefix_lists/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/prefix_lists/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/prefix_lists/prefix_lists.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/prefix_lists/prefix_lists.py new file mode 100644 index 000000000..d5c36d3e2 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/prefix_lists/prefix_lists.py @@ -0,0 +1,458 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_prefix_lists class +It is in this file that 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.module_utils.connection import ConnectionError + +TEST_KEYS = [ + {"config": {"afi": "", "name": ""}}, + {"prefixes": {"action": "", "ge": "", "le": "", "prefix": "", "sequence": ""}} +] + +DELETE = "delete" +PATCH = "patch" + + +class Prefix_lists(ConfigBase): + """ + The sonic_prefix_lists class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'prefix_lists', + ] + + prefix_sets_uri = 'data/openconfig-routing-policy:routing-policy/defined-sets/prefix-sets' + prefix_set_uri = 'data/openconfig-routing-policy:routing-policy/defined-sets/\ +prefix-sets/prefix-set' + prefix_set_delete_uri = 'data/openconfig-routing-policy:routing-policy/defined-sets/\ +prefix-sets/prefix-set={}' + prefix_set_delete_all_prefixes_uri = 'data/openconfig-routing-policy:routing-policy/\ +defined-sets/prefix-sets/prefix-set={}/openconfig-routing-policy-ext:extended-prefixes' + prefix_set_delete_prefix_uri = 'data/openconfig-routing-policy:routing-policy/\ +defined-sets/prefix-sets/prefix-set={}/\ +openconfig-routing-policy-ext:extended-prefixes/extended-prefix={},{},{}' + prefix_set_data_path = 'openconfig-routing-policy:prefix-set' + ext_prefix_set_data_path = 'openconfig-routing-policy-ext:extended-prefixes' + + def __init__(self, module): + super(Prefix_lists, self).__init__(module) + + def get_prefix_lists_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) + prefix_lists_facts = facts['ansible_network_resources'].get('prefix_lists', None) + if not prefix_lists_facts: + return [] + return prefix_lists_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_prefix_lists_facts = self.get_prefix_lists_facts() + commands, requests = self.set_config(existing_prefix_lists_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_prefix_lists_facts = self.get_prefix_lists_facts() + + result['before'] = existing_prefix_lists_facts + if result['changed']: + result['after'] = changed_prefix_lists_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_prefix_lists_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_prefix_lists_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + diff = get_diff(want, have, TEST_KEYS) + if state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(diff) + ret_commands = commands + return ret_commands, requests + + def _state_merged(self, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_prefix_lists_requests(commands) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + 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 = list() + if not want or want == []: + commands = have + requests = self.get_delete_all_prefix_list_cfg_requests() + else: + commands = want + requests = self.get_delete_prefix_lists_cfg_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + return commands, requests + + def get_modify_prefix_lists_requests(self, commands): + '''Traverse the input list of configuration "modify" commands obtained + from parsing the input playbook parameters. For each command, + create and return the appropriate set of REST API requests to modify + the prefix set specified by the current command.''' + + requests = [] + if not commands: + return requests + + # Create URL and payload + prefix_set_payload_list = [] + for command in commands: + prefix_set_payload = self.get_modify_single_prefix_set_request(command) + if prefix_set_payload: + prefix_set_payload_list.append(prefix_set_payload) + prefix_set_data = {self.prefix_set_data_path: prefix_set_payload_list} + request = {'path': self.prefix_set_uri, 'method': PATCH, 'data': prefix_set_data} + requests.append(request) + return requests + + def get_modify_single_prefix_set_request(self, command): + '''Create and return the appropriate set of REST API requests to modfy + the prefix set configuration specified by the current "command".''' + + request = {} + if not command: + return request + + conf_afi = command.get('afi', None) + conf_name = command.get('name', None) + if not conf_afi or not conf_name: + return request + + prefix_set_payload_header = {'name': conf_name, + 'config': {'name': conf_name, 'mode': conf_afi.upper()}} + + pfx_conf_list = [] + prefixes = command.get('prefixes', None) + + if prefixes: + for prefix in prefixes: + pfx_payload = self.get_modify_prefix_request(prefix, conf_afi) + if pfx_payload: + pfx_conf_list.append(pfx_payload) + + ext_prefix_list_payload = {'extended-prefix': pfx_conf_list} + ext_prefix_list_data = {self.ext_prefix_set_data_path: ext_prefix_list_payload} + + prefix_set_payload = prefix_set_payload_header + prefix_set_payload.update(ext_prefix_list_data) + return prefix_set_payload + + def get_modify_prefix_request(self, prefix, conf_afi): + '''Create a REST API request to update/merge/create the prefix specified by the + "prefix" input parameter.''' + + pfx_payload = {} + prefix_val = prefix.get('prefix', None) + sequence = prefix.get('sequence', None) + action = prefix.get('action', None) + if not prefix_val or not sequence or not action: + return None + + prefix_net = self.set_ipaddress_net_attrs(prefix_val, conf_afi) + ge = prefix.get('ge', None) + le = prefix.get('le', None) + pfx_payload['ip-prefix'] = prefix_val + pfx_payload['sequence-number'] = sequence + masklength_range_str = self.get_masklength_range_string(ge, le, prefix_net) + pfx_payload['masklength-range'] = masklength_range_str + pfx_config = {} + pfx_config['sequence-number'] = sequence + pfx_config['ip-prefix'] = prefix_val + pfx_config['masklength-range'] = pfx_payload['masklength-range'] + pfx_config['openconfig-routing-policy-ext:action'] = action.upper() + pfx_payload['config'] = pfx_config + + return pfx_payload + + def get_create_prefix_lists_cfg_requests(self, commands): + '''Placeholder function Modify this function if necessary to enable + separate actions for "CREATE" vs "MERGE" ("PATCH") requests''' + + return self.get_modify_prefix_lists_requests(commands) + + def get_delete_prefix_lists_cfg_requests(self, commands, have): + '''Traverse the input list of configuration "delete" commands obtained + from parsing the input playbook parameters. For each command, + create and return the appropriate set of REST API requests to delete + the prefix set configuration specified by the current "command".''' + requests = [] + for command in commands: + new_requests = self.get_delete_single_prefix_cfg_requests(command, have) + if new_requests and len(new_requests) > 0: + requests.extend(new_requests) + return requests + + def get_delete_single_prefix_cfg_requests(self, command, have): + '''Create and return the appropriate set of REST API requests to delete + the prefix set configuration specified by the current "command".''' + + requests = list() + pfx_set_name = command.get('name', None) + if not pfx_set_name: + return requests + + cfg_prefix_set = self.prefix_set_in_config(pfx_set_name, have) + if not cfg_prefix_set: + return requests + + prefixes = command.get('prefixes', None) + if not prefixes or prefixes == []: + requests = self.get_delete_prefix_set_cfg(command) + else: + requests = self.get_delete_one_prefix_list_cfg(cfg_prefix_set, command) + return requests + + def get_delete_prefix_set_cfg(self, command): + '''Create and return a REST API request to delete the prefix set specified + by the current "command".''' + + pfx_set_name = command.get('name', None) + + requests = [{'path': self.prefix_set_delete_uri.format(pfx_set_name), 'method': DELETE}] + return requests + + def get_delete_one_prefix_list_cfg(self, cfg_prefix_set, command): + '''Create the list of REST API prefix deletion requests needed for deletion + of the the requested set of prefixes from the currently configured + prefix set specified by "cfg_prefix_set".''' + + pfx_delete_cfg_list = list() + prefixes = command.get('prefixes', None) + + for prefix in prefixes: + pfx_delete_cfg = self.prefix_get_delete_single_prefix_cfg(prefix, + cfg_prefix_set, + command) + if pfx_delete_cfg and len(pfx_delete_cfg) > 0: + pfx_delete_cfg_list.append(pfx_delete_cfg) + return pfx_delete_cfg_list + + def prefix_get_delete_single_prefix_cfg(self, prefix, cfg_prefix_set, command): + '''Create the REST API request to delete the prefix specified by the "prefix" + input parameter from the configured prefix set specified by "cfg_prefix_set". + Return an empty request if the prefix is not present in the confgured prefix set.''' + + pfx_delete_cfg_request = {} + if not self.prefix_in_prefix_list_cfg(prefix, cfg_prefix_set): + return pfx_delete_cfg_request + + conf_afi = command.get('afi', None) + if not conf_afi: + return pfx_delete_cfg_request + + pfx_set_name = command.get('name', None) + pfx_seq = prefix.get("sequence", None) + pfx_val = prefix.get("prefix", None) + pfx_ge = prefix.get("ge", None) + pfx_le = prefix.get("le", None) + + if not pfx_seq or not pfx_val: + return pfx_delete_cfg_request + + prefix_net = self.set_ipaddress_net_attrs(pfx_val, conf_afi) + masklength_range_str = self.get_masklength_range_string(pfx_ge, pfx_le, prefix_net) + prefix_string = pfx_val.replace("/", "%2F") + extended_pfx_cfg_str = self.prefix_set_delete_prefix_uri.format(pfx_set_name, + int(pfx_seq), + prefix_string, + masklength_range_str) + pfx_delete_cfg_request = {'path': extended_pfx_cfg_str, 'method': DELETE} + return pfx_delete_cfg_request + + def get_delete_all_prefix_list_cfg_requests(self): + '''Delete all prefix list configuration''' + requests = list() + requests = [{'path': self.prefix_sets_uri, 'method': DELETE}] + return requests + + def get_masklength_range_string(self, pfx_ge, pfx_le, prefix_net): + '''Determine the "masklength range" string required for the openconfig + REST API to configure the affected prefix.''' + if not pfx_ge and not pfx_le: + masklength_range_string = "exact" + elif pfx_ge and not pfx_le: + masklength_range_string = str(pfx_ge) + ".." + str(prefix_net['max_prefixlen']) + elif not pfx_ge and pfx_le: + masklength_range_string = str(prefix_net['prefixlen']) + ".." + str(pfx_le) + else: + masklength_range_string = str(pfx_ge) + ".." + str(pfx_le) + + return masklength_range_string + + def prefix_set_in_config(self, pfx_set_name, have): + '''Determine if the prefix set specifid by "pfx_set_name" is present in + the current switch configuration. If it is present, return the "found" + prefix set. (Otherwise, return "None"''' + for cfg_prefix_set in have: + cfg_prefix_set_name = cfg_prefix_set.get('name', None) + if cfg_prefix_set_name and cfg_prefix_set_name == pfx_set_name: + return cfg_prefix_set + + return None + + def prefix_in_prefix_list_cfg(self, prefix, cfg_prefix_set): + '''Determine, based on the keys, if the "target" prefix specified by the "prefix" + input parameter is present in the currently configured prefix set specified + ty the "cfg_prefix_set" input parameter. Return "True" if the prifix is found, + or "False" if it isn't.''' + req_pfx = prefix.get("prefix", None) + req_seq = prefix.get("sequence", None) + req_ge = prefix.get("ge", None) + req_le = prefix.get("le", None) + + cfg_prefix_list = cfg_prefix_set.get("prefixes", None) + if not cfg_prefix_list: # The configured prefix set has no prefix list + return False + + for cfg_prefix in cfg_prefix_list: + cfg_pfx = cfg_prefix.get("prefix", None) + cfg_seq = cfg_prefix.get("sequence", None) + cfg_ge = cfg_prefix.get("ge", None) + cfg_le = cfg_prefix.get("le", None) + + # Check for matching key attributes + if not (req_pfx and cfg_pfx and req_pfx == cfg_pfx): + continue + if not (req_seq and cfg_seq and req_seq == cfg_seq): + continue + + # Check for ge match + if not req_ge: + if cfg_ge: + continue + else: + if not cfg_ge or req_ge != cfg_ge: + continue + + # Check for le match + if not req_le: + if cfg_le: + continue + else: + if not cfg_le or req_le != cfg_le: + continue + + # All key attributes match for this cfg_prefix + return True + + # No matching configured prefixes were found in the prefix set. + return False + + def set_ipaddress_net_attrs(self, prefix_val, conf_afi): + '''Create and return a dictionary containing the values for any prefix-related + attributes needed for handling of prefix configuration requests. NOTE: This + method should be replaced with use of the Python "ipaddress" module after + Ansible drops downward compatibility support for Python 2.7.''' + + prefix_net = dict() + if conf_afi == 'ipv4': + prefix_net['max_prefixlen'] = 32 + else: # Assuming IPv6 for this case + prefix_net['max_prefixlen'] = 128 + + prefix_net['prefixlen'] = int(prefix_val.split("/")[1]) + return prefix_net diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/radius_server/radius_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/radius_server/radius_server.py new file mode 100644 index 000000000..dfa65482f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/radius_server/radius_server.py @@ -0,0 +1,362 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_radius_server 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.module_utils.connection import ConnectionError +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, + normalize_interface_name, +) + +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'host': {'name': ''}}, +] + + +class Radius_server(ConfigBase): + """ + The sonic_radius_server class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'radius_server', + ] + + def __init__(self, module): + super(Radius_server, self).__init__(module) + + def get_radius_server_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) + radius_server_facts = facts['ansible_network_resources'].get('radius_server') + if not radius_server_facts: + return [] + return radius_server_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_radius_server_facts = self.get_radius_server_facts() + commands, requests = self.set_config(existing_radius_server_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_radius_server_facts = self.get_radius_server_facts() + + result['before'] = existing_radius_server_facts + if result['changed']: + result['after'] = changed_radius_server_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_radius_server_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'] + + if want and want.get('servers', None) and want['servers'].get('host', None): + normalize_interface_name(want['servers']['host'], self._module, 'source_interface') + + have = existing_radius_server_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + if not want: + want = {} + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + command = diff + requests = self.get_modify_radius_server_requests(command, have) + if command and len(requests) > 0: + commands = update_states([command], "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the radius_serveri except admin + commands = [] + if not want: + command = have + else: + command = want + + requests = self.get_delete_radius_server_requests(command, have) + + if command and len(requests) > 0: + commands = update_states([command], "deleted") + + return commands, requests + + def get_radius_global_payload(self, conf): + payload = {} + global_cfg = {} + + if conf.get('auth_type', None): + global_cfg['auth-type'] = conf['auth_type'] + if conf.get('key', None): + global_cfg['secret-key'] = conf['key'] + if conf.get('timeout', None): + global_cfg['timeout'] = conf['timeout'] + + if global_cfg: + payload = {'openconfig-system:config': global_cfg} + + return payload + + def get_radius_global_ext_payload(self, conf): + payload = {} + global_ext_cfg = {} + + if conf.get('nas_ip', None): + global_ext_cfg['nas-ip-address'] = conf['nas_ip'] + if conf.get('retransmit', None): + global_ext_cfg['retransmit-attempts'] = conf['retransmit'] + if conf.get('statistics', None): + global_ext_cfg['statistics'] = conf['statistics'] + + if global_ext_cfg: + payload = {'openconfig-aaa-radius-ext:config': global_ext_cfg} + + return payload + + def get_radius_server_payload(self, hosts): + payload = {} + servers_load = [] + for host in hosts: + if host.get('name', None): + host_cfg = {'address': host['name']} + if host.get('auth_type', None): + host_cfg['auth-type'] = host['auth_type'] + if host.get('priority', None): + host_cfg['priority'] = host['priority'] + if host.get('vrf', None): + host_cfg['vrf'] = host['vrf'] + if host.get('timeout', None): + host_cfg['timeout'] = host['timeout'] + + radius_port_key_cfg = {} + if host.get('port', None): + radius_port_key_cfg['auth-port'] = host['port'] + if host.get('key', None): + radius_port_key_cfg['secret-key'] = host['key'] + if host.get('retransmit', None): + radius_port_key_cfg['retransmit-attempts'] = host['retransmit'] + if host.get('source_interface', None): + radius_port_key_cfg['openconfig-aaa-radius-ext:source-interface'] = host['source_interface'] + + if radius_port_key_cfg: + consolidated_load = {'address': host['name']} + consolidated_load['config'] = host_cfg + consolidated_load['radius'] = {'config': radius_port_key_cfg} + servers_load.append(consolidated_load) + + if servers_load: + payload = {'openconfig-system:servers': {'server': servers_load}} + + return payload + + def get_modify_servers_request(self, command): + request = None + + hosts = [] + if command.get('servers', None) and command['servers'].get('host', None): + hosts = command['servers']['host'] + if hosts: + url = 'data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/servers' + payload = self.get_radius_server_payload(hosts) + if payload: + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_global_config_request(self, conf): + request = None + + url = 'data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/config' + payload = self.get_radius_global_payload(conf) + if payload: + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_global_ext_config_request(self, conf): + request = None + + url = 'data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/openconfig-aaa-radius-ext:radius/config' + payload = self.get_radius_global_ext_payload(conf) + if payload: + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_radius_server_requests(self, command, have): + requests = [] + if not command: + return requests + + request = self.get_modify_global_config_request(command) + if request: + requests.append(request) + + request = self.get_modify_global_ext_config_request(command) + if request: + requests.append(request) + + request = self.get_modify_servers_request(command) + if request: + requests.append(request) + + return requests + + def get_delete_global_ext_params(self, conf, match): + + requests = [] + + url = 'data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/openconfig-aaa-radius-ext:radius/config/' + if conf.get('nas_ip', None) and match.get('nas_ip', None): + requests.append({'path': url + 'nas-ip-address', 'method': DELETE}) + if conf.get('retransmit', None) and match.get('retransmit', None): + requests.append({'path': url + 'retransmit-attempts', 'method': DELETE}) + if conf.get('statistics', None) and match.get('statistics', None): + requests.append({'path': url + 'statistics', 'method': DELETE}) + + return requests + + def get_delete_global_params(self, conf, match): + + requests = [] + + url = 'data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/config/' + if conf.get('auth_type', None) and match.get('auth_type', None) and match['auth_type'] != 'pap': + requests.append({'path': url + 'auth-type', 'method': DELETE}) + if conf.get('key', None) and match.get('key', None): + requests.append({'path': url + 'secret-key', 'method': DELETE}) + if conf.get('timeout', None) and match.get('timeout', None) and match['timeout'] != 5: + requests.append({'path': url + 'timeout', 'method': DELETE}) + + return requests + + def get_delete_servers(self, command, have): + requests = [] + url = 'data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/servers/server=' + + mat_hosts = [] + if have.get('servers', None) and have['servers'].get('host', None): + mat_hosts = have['servers']['host'] + + if command.get('servers', None): + if command['servers'].get('host', None): + hosts = command['servers']['host'] + else: + hosts = mat_hosts + + if mat_hosts and hosts: + for host in hosts: + if next((m_host for m_host in mat_hosts if m_host['name'] == host['name']), None): + requests.append({'path': url + host['name'], 'method': DELETE}) + + return requests + + def get_delete_radius_server_requests(self, command, have): + requests = [] + if not command: + return requests + + requests.extend(self.get_delete_global_params(command, have)) + requests.extend(self.get_delete_global_ext_params(command, have)) + requests.extend(self.get_delete_servers(command, have)) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/static_routes/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/static_routes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/static_routes/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/static_routes/static_routes.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/static_routes/static_routes.py new file mode 100644 index 000000000..047357470 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/static_routes/static_routes.py @@ -0,0 +1,344 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 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_static_routes 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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, +) + +network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' +protocol_static_routes_path = 'protocols/protocol=STATIC,static/static-routes' + +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'config': {'vrf_name': ''}}, + {'static_list': {'prefix': ''}}, + {'next_hops': {'index': ''}}, +] + + +class Static_routes(ConfigBase): + """ + The sonic_static_routes class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'static_routes', + ] + + def __init__(self, module): + super(Static_routes, self).__init__(module) + + def get_static_routes_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) + static_routes_facts = facts['ansible_network_resources'].get('static_routes') + if not static_routes_facts: + return [] + return static_routes_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + commands = [] + existing_static_routes_facts = self.get_static_routes_facts() + commands, requests = self.set_config(existing_static_routes_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_static_routes_facts = self.get_static_routes_facts() + + result['before'] = existing_static_routes_facts + if result['changed']: + result['after'] = changed_static_routes_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_static_routes_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_static_routes_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 + """ + + commands = [] + requests = [] + state = self._module.params['state'] + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = self.get_modify_static_routes_requests(commands, have) + + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + is_delete_all = False + # if want is none, then delete ALL + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests = self.get_delete_static_routes_requests(commands, have, is_delete_all) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_modify_static_routes_requests(self, commands, have): + requests = [] + + if not commands: + return requests + + for conf in commands: + vrf_name = conf.get('vrf_name', None) + static_list = conf.get('static_list', []) + for static in static_list: + prefix = static.get('prefix', None) + next_hops = static.get('next_hops', []) + if next_hops: + for next_hop in next_hops: + requests.append(self.get_modify_static_route_request(vrf_name, prefix, next_hop)) + + return requests + + def get_modify_static_route_request(self, vrf_name, prefix, next_hop): + request = None + next_hop_cfg = {} + index = next_hop.get('index', {}) + blackhole = index.get('blackhole', None) + interface = index.get('interface', None) + nexthop_vrf = index.get('nexthop_vrf', None) + next_hop_attr = index.get('next_hop', None) + metric = next_hop.get('metric', None) + track = next_hop.get('track', None) + tag = next_hop.get('tag', None) + idx = self.generate_index(index) + if idx: + next_hop_cfg['index'] = idx + if blackhole: + next_hop_cfg['blackhole'] = blackhole + if nexthop_vrf: + next_hop_cfg['network-instance'] = nexthop_vrf + if next_hop: + next_hop_cfg['next-hop'] = next_hop_attr + if metric: + next_hop_cfg['metric'] = metric + if track: + next_hop_cfg['track'] = track + if tag: + next_hop_cfg['tag'] = tag + + url = '%s=%s/%s' % (network_instance_path, vrf_name, protocol_static_routes_path) + next_hops_cfg = {'next-hop': [{'index': idx, 'config': next_hop_cfg}]} + if interface: + next_hops_cfg['next-hop'][0]['interface-ref'] = {'config': {'interface': interface}} + payload = {'openconfig-network-instance:static-routes': {'static': [{'prefix': prefix, 'config': {'prefix': prefix}, 'next-hops': next_hops_cfg}]}} + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def generate_index(self, index): + idx = None + blackhole = index.get('blackhole', None) + interface = index.get('interface', None) + nexthop_vrf = index.get('nexthop_vrf', None) + next_hop = index.get('next_hop', None) + + if blackhole is True: + idx = 'DROP' + else: + if interface: + if not next_hop and not nexthop_vrf: + idx = interface + elif next_hop and not nexthop_vrf: + idx = interface + '_' + next_hop + elif nexthop_vrf and not next_hop: + idx = interface + '_' + nexthop_vrf + else: + idx = interface + '_' + next_hop + '_' + nexthop_vrf + else: + if next_hop and not nexthop_vrf: + idx = next_hop + elif next_hop and nexthop_vrf: + idx = next_hop + '_' + nexthop_vrf + + return idx + + def get_delete_static_routes_requests(self, commands, have, is_delete_all): + requests = [] + if is_delete_all: + for cmd in commands: + vrf_name = cmd.get('vrf_name', None) + if vrf_name: + requests.append(self.get_delete_static_routes_for_vrf(vrf_name)) + else: + for cmd in commands: + vrf_name = cmd.get('vrf_name', None) + static_list = cmd.get('static_list', []) + for cfg in have: + cfg_vrf_name = cfg.get('vrf_name', None) + if vrf_name == cfg_vrf_name: + if not static_list: + requests.append(self.get_delete_static_routes_for_vrf(vrf_name)) + else: + for static in static_list: + prefix = static.get('prefix', None) + next_hops = static.get('next_hops', []) + cfg_static_list = cfg.get('static_list', []) + for cfg_static in cfg_static_list: + cfg_prefix = cfg_static.get('prefix', None) + if prefix == cfg_prefix: + if prefix and not next_hops: + requests.append(self.get_delete_static_routes_prefix_request(vrf_name, prefix)) + else: + for next_hop in next_hops: + index = next_hop.get('index', {}) + idx = self.generate_index(index) + metric = next_hop.get('metric', None) + track = next_hop.get('track', None) + tag = next_hop.get('tag', None) + + cfg_next_hops = cfg_static.get('next_hops', []) + if cfg_next_hops: + for cfg_next_hop in cfg_next_hops: + cfg_index = cfg_next_hop.get('index', {}) + cfg_idx = self.generate_index(cfg_index) + if idx == cfg_idx: + cfg_metric = cfg_next_hop.get('metric', None) + cfg_track = cfg_next_hop.get('track', None) + cfg_tag = cfg_next_hop.get('tag', None) + if not metric and not track and not tag: + requests.append(self.get_delete_static_routes_next_hop_request(vrf_name, prefix, idx)) + else: + if metric == cfg_metric: + requests.append(self.get_delete_next_hop_config_attr_request(vrf_name, prefix, idx, + 'metric')) + if track == cfg_track: + requests.append(self.get_delete_next_hop_config_attr_request(vrf_name, prefix, idx, + 'track')) + if tag == cfg_tag: + requests.append(self.get_delete_next_hop_config_attr_request(vrf_name, prefix, idx, 'tag')) + + return requests + + def get_delete_static_routes_for_vrf(self, vrf_name): + url = '%s=%s/%s' % (network_instance_path, vrf_name, protocol_static_routes_path) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_static_routes_prefix_request(self, vrf_name, prefix): + prefix = prefix.replace('/', '%2F') + url = '%s=%s/%s/static=%s' % (network_instance_path, vrf_name, protocol_static_routes_path, prefix) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_static_routes_next_hop_request(self, vrf_name, prefix, index): + prefix = prefix.replace('/', '%2F') + url = '%s=%s/%s/static=%s' % (network_instance_path, vrf_name, protocol_static_routes_path, prefix) + url += '/next-hops/next-hop=%s' % (index) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_next_hop_config_attr_request(self, vrf_name, prefix, index, attr): + prefix = prefix.replace('/', '%2F') + url = '%s=%s/%s/static=%s' % (network_instance_path, vrf_name, protocol_static_routes_path, prefix) + url += '/next-hops/next-hop=%s/config/%s' % (index, attr) + request = {'path': url, 'method': DELETE} + + return request diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/system/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/system/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/system/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/system/system.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/system/system.py new file mode 100644 index 000000000..21d575a1f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/system/system.py @@ -0,0 +1,294 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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_system 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.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +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 ( + update_states, + get_diff, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + +PATCH = 'patch' +DELETE = 'delete' + + +class System(ConfigBase): + """ + The sonic_system class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'system', + ] + + def __init__(self, module): + super(System, self).__init__(module) + + def get_system_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) + system_facts = facts['ansible_network_resources'].get('system') + if not system_facts: + return [] + return system_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_system_facts = self.get_system_facts() + commands, requests = self.set_config(existing_system_facts) + if commands and len(requests) > 0: + if not self._module.check_mode: + self.edit_config(requests) + result['changed'] = True + result['commands'] = commands + + changed_system_facts = self.get_system_facts() + + result['before'] = existing_system_facts + if result['changed']: + result['after'] = changed_system_facts + + result['warnings'] = warnings + return result + + def edit_config(self, requests): + try: + response = edit_config(self._module, to_request(self._module, requests)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + def set_config(self, existing_system_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_system_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 == 'deleted': + commands = self._state_deleted(want, have) + elif state == 'merged': + diff = get_diff(want, have) + commands = self._state_merged(want, have, diff) + return commands + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + requests = [] + commands = [] + if diff: + requests = self.get_create_system_request(want, diff) + if len(requests) > 0: + commands = update_states(diff, "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 = [] + new_have = self.remove_default_entries(have) + if not want: + if have: + requests = self.get_delete_all_system_request(new_have) + if len(requests) > 0: + commands = update_states(have, "deleted") + else: + want = utils.remove_empties(want) + d_diff = get_diff(want, new_have, is_skeleton=True) + diff_want = get_diff(want, d_diff, is_skeleton=True) + if diff_want: + requests = self.get_delete_all_system_request(diff_want) + if len(requests) > 0: + commands = update_states(diff_want, "deleted") + return commands, requests + + def get_create_system_request(self, want, commands): + requests = [] + host_path = 'data/openconfig-system:system/config' + method = PATCH + hostname_payload = self.build_create_hostname_payload(commands) + if hostname_payload: + request = {'path': host_path, 'method': method, 'data': hostname_payload} + requests.append(request) + name_path = 'data/sonic-device-metadata:sonic-device-metadata/DEVICE_METADATA/DEVICE_METADATA_LIST=localhost/intf_naming_mode' + name_payload = self.build_create_name_payload(commands) + if name_payload: + request = {'path': name_path, 'method': method, 'data': name_payload} + requests.append(request) + anycast_path = 'data/sonic-sag:sonic-sag/SAG_GLOBAL/SAG_GLOBAL_LIST/' + anycast_payload = self.build_create_anycast_payload(commands) + if anycast_payload: + request = {'path': anycast_path, 'method': method, 'data': anycast_payload} + requests.append(request) + return requests + + def build_create_hostname_payload(self, commands): + payload = {"openconfig-system:config": {}} + if "hostname" in commands and commands["hostname"]: + payload['openconfig-system:config'].update({"hostname": commands["hostname"]}) + return payload + + def build_create_name_payload(self, commands): + payload = {} + if "interface_naming" in commands and commands["interface_naming"]: + payload.update({'sonic-device-metadata:intf_naming_mode': commands["interface_naming"]}) + return payload + + def build_create_anycast_payload(self, commands): + payload = {} + if "anycast_address" in commands and commands["anycast_address"]: + payload = {"sonic-sag:SAG_GLOBAL_LIST": []} + temp = {} + if "ipv4" in commands["anycast_address"] and commands["anycast_address"]["ipv4"]: + temp.update({'IPv4': "enable"}) + if "ipv4" in commands["anycast_address"] and not commands["anycast_address"]["ipv4"]: + temp.update({'IPv4': "disable"}) + if "ipv6" in commands["anycast_address"] and commands["anycast_address"]["ipv6"]: + temp.update({'IPv6': "enable"}) + if "ipv6" in commands["anycast_address"] and not commands["anycast_address"]["ipv6"]: + temp.update({'IPv6': "disable"}) + if "mac_address" in commands["anycast_address"] and commands["anycast_address"]["mac_address"]: + temp.update({'gwmac': commands["anycast_address"]["mac_address"]}) + if temp: + temp.update({"table_distinguisher": "IP"}) + payload["sonic-sag:SAG_GLOBAL_LIST"].append(temp) + return payload + + def remove_default_entries(self, data): + new_data = {} + if not data: + return new_data + else: + hostname = data.get('hostname', None) + if hostname != "sonic": + new_data["hostname"] = hostname + intf_name = data.get('interface_naming', None) + if intf_name != "native": + new_data["interface_naming"] = intf_name + new_anycast = {} + anycast = data.get('anycast_address', None) + if anycast: + ipv4 = anycast.get("ipv4", None) + if ipv4 is not True: + new_anycast["ipv4"] = ipv4 + ipv6 = anycast.get("ipv6", None) + if ipv6 is not True: + new_anycast["ipv6"] = ipv6 + mac = anycast.get("mac_address", None) + if mac is not None: + new_anycast["mac_address"] = mac + new_data["anycast_address"] = new_anycast + return new_data + + def get_delete_all_system_request(self, have): + requests = [] + if "hostname" in have: + request = self.get_hostname_delete_request() + requests.append(request) + if "interface_naming" in have: + request = self.get_intfname_delete_request() + requests.append(request) + if "anycast_address" in have: + request = self.get_anycast_delete_request(have["anycast_address"]) + requests.extend(request) + return requests + + def get_hostname_delete_request(self): + path = 'data/openconfig-system:system/config/' + method = PATCH + payload = {"openconfig-system:config": {}} + payload['openconfig-system:config'].update({"hostname": "sonic"}) + request = {'path': path, 'method': method, 'data': payload} + return request + + def get_intfname_delete_request(self): + path = 'data/sonic-device-metadata:sonic-device-metadata/DEVICE_METADATA/DEVICE_METADATA_LIST=localhost/intf_naming_mode' + method = DELETE + request = {'path': path, 'method': method} + return request + + def get_anycast_delete_request(self, anycast): + requests = [] + if "ipv4" in anycast: + path = 'data/sonic-sag:sonic-sag/SAG_GLOBAL/SAG_GLOBAL_LIST=IP/IPv4' + method = DELETE + request = {'path': path, 'method': method} + requests.append(request) + if "ipv6" in anycast: + path = 'data/sonic-sag:sonic-sag/SAG_GLOBAL/SAG_GLOBAL_LIST=IP/IPv6' + method = DELETE + request = {'path': path, 'method': method} + requests.append(request) + if "mac_address" in anycast: + path = 'data/sonic-sag:sonic-sag/SAG_GLOBAL/SAG_GLOBAL_LIST=IP/gwmac' + method = DELETE + request = {'path': path, 'method': method} + requests.append(request) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/tacacs_server/tacacs_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/tacacs_server/tacacs_server.py new file mode 100644 index 000000000..498fcbe28 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/tacacs_server/tacacs_server.py @@ -0,0 +1,318 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_tacacs_server 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.module_utils.connection import ConnectionError +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, + get_normalize_interface_name, +) + +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'host': {'name': ''}}, +] + + +class Tacacs_server(ConfigBase): + """ + The sonic_tacacs_server class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'tacacs_server', + ] + + def __init__(self, module): + super(Tacacs_server, self).__init__(module) + + def get_tacacs_server_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) + tacacs_server_facts = facts['ansible_network_resources'].get('tacacs_server') + if not tacacs_server_facts: + return [] + return tacacs_server_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_tacacs_server_facts = self.get_tacacs_server_facts() + commands, requests = self.set_config(existing_tacacs_server_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_tacacs_server_facts = self.get_tacacs_server_facts() + + result['before'] = existing_tacacs_server_facts + if result['changed']: + result['after'] = changed_tacacs_server_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_tacacs_server_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'] + + if want and want.get('source_interface', None): + want['source_interface'] = get_normalize_interface_name(want['source_interface'], self._module) + + have = existing_tacacs_server_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + if not want: + want = {} + + diff = get_diff(want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + command = diff + requests = self.get_modify_tacacs_server_requests(command, have) + if command and len(requests) > 0: + commands = update_states([command], "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the tacacs_serveri except admin + commands = [] + if not want: + command = have + else: + command = want + + requests = self.get_delete_tacacs_server_requests(command, have) + + if command and len(requests) > 0: + commands = update_states([command], "deleted") + + return commands, requests + + def get_tacacs_global_payload(self, conf): + payload = {} + global_cfg = {} + + if conf.get('auth_type', None): + global_cfg['auth-type'] = conf['auth_type'] + if conf.get('key', None): + global_cfg['secret-key'] = conf['key'] + if conf.get('source_interface', None): + global_cfg['source-interface'] = conf['source_interface'] + if conf.get('timeout', None): + global_cfg['timeout'] = conf['timeout'] + + if global_cfg: + payload = {'openconfig-system:config': global_cfg} + + return payload + + def get_tacacs_server_payload(self, hosts): + payload = {} + servers_load = [] + for host in hosts: + if host.get('name', None): + host_cfg = {'address': host['name']} + if host.get('auth_type', None): + host_cfg['auth-type'] = host['auth_type'] + if host.get('priority', None): + host_cfg['priority'] = host['priority'] + if host.get('vrf', None): + host_cfg['vrf'] = host['vrf'] + if host.get('timeout', None): + host_cfg['timeout'] = host['timeout'] + + tacacs_port_key_cfg = {} + if host.get('port', None): + tacacs_port_key_cfg['port'] = host['port'] + if host.get('key', None): + tacacs_port_key_cfg['secret-key'] = host['key'] + + if tacacs_port_key_cfg: + consolidated_load = {'address': host['name']} + consolidated_load['config'] = host_cfg + consolidated_load['tacacs'] = {'config': tacacs_port_key_cfg} + servers_load.append(consolidated_load) + + if servers_load: + payload = {'openconfig-system:servers': {'server': servers_load}} + + return payload + + def get_modify_servers_request(self, command): + request = None + + hosts = [] + if command.get('servers', None) and command['servers'].get('host', None): + hosts = command['servers']['host'] + if hosts: + url = 'data/openconfig-system:system/aaa/server-groups/server-group=TACACS/servers' + payload = self.get_tacacs_server_payload(hosts) + if payload: + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_global_config_request(self, conf): + request = None + + url = 'data/openconfig-system:system/aaa/server-groups/server-group=TACACS/config' + payload = self.get_tacacs_global_payload(conf) + if payload: + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_tacacs_server_requests(self, command, have): + requests = [] + if not command: + return requests + + request = self.get_modify_global_config_request(command) + if request: + requests.append(request) + + request = self.get_modify_servers_request(command) + if request: + requests.append(request) + + return requests + + def get_delete_global_params(self, conf, match): + + requests = [] + + url = 'data/openconfig-system:system/aaa/server-groups/server-group=TACACS/config/' + if conf.get('auth_type', None) and match.get('auth_type', None) and match['auth_type'] != 'pap': + requests.append({'path': url + 'auth-type', 'method': DELETE}) + if conf.get('key', None) and match.get('key', None): + requests.append({'path': url + 'secret-key', 'method': DELETE}) + if conf.get('source_interface', None) and match.get('source_interface', None): + requests.append({'path': url + 'source-interface', 'method': DELETE}) + if conf.get('timeout', None) and match.get('timeout', None) and match['timeout'] != 5: + requests.append({'path': url + 'timeout', 'method': DELETE}) + + return requests + + def get_delete_servers(self, command, have): + requests = [] + url = 'data/openconfig-system:system/aaa/server-groups/server-group=TACACS/servers/server=' + + mat_hosts = [] + if have.get('servers', None) and have['servers'].get('host', None): + mat_hosts = have['servers']['host'] + + hosts = [] + if command.get('servers', None): + if command['servers'].get('host', None): + hosts = command['servers']['host'] + else: + hosts = mat_hosts + + if mat_hosts and hosts: + for host in hosts: + if next((m_host for m_host in mat_hosts if m_host['name'] == host['name']), None): + requests.append({'path': url + host['name'], 'method': DELETE}) + + return requests + + def get_delete_tacacs_server_requests(self, command, have): + requests = [] + if not command: + return requests + + requests.extend(self.get_delete_global_params(command, have)) + requests.extend(self.get_delete_servers(command, have)) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/users/users.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/users/users.py new file mode 100644 index 000000000..73398cf74 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/users/users.py @@ -0,0 +1,299 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_users 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 +import json + +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + dict_to_set, + update_states, + get_diff, +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'delete' + + +class Users(ConfigBase): + """ + The sonic_users class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'users', + ] + + def __init__(self, module): + super(Users, self).__init__(module) + + def get_users_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) + users_facts = facts['ansible_network_resources'].get('users') + if not users_facts: + return [] + return users_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_users_facts = self.get_users_facts() + commands, requests = self.set_config(existing_users_facts) + auth_error = False + 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: + try: + json_obj = json.loads(str(exc).replace("'", '"')) + if json_obj and type(json_obj) is dict and 401 == json_obj['code']: + auth_error = True + warnings.append("Unable to get after configs as password got changed for current user") + else: + self._module.fail_json(msg=str(exc), code=exc.code) + except Exception as err: + self._module.fail_json(msg=str(exc), code=exc.code) + result['changed'] = True + result['commands'] = commands + + changed_users_facts = [] + if not auth_error: + changed_users_facts = self.get_users_facts() + + result['before'] = existing_users_facts + if result['changed']: + result['after'] = changed_users_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_users_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_users_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 + """ + commands = [] + requests = [] + state = self._module.params['state'] + if not want: + want = [] + + new_want = [{'name': conf['name'], 'role': conf['role']} for conf in want] + new_have = [{'name': conf['name'], 'role': conf['role']} for conf in have] + new_diff = get_diff(new_want, new_have) + + diff = [] + for cfg in new_diff: + match = next((w_cfg for w_cfg in want if w_cfg['name'] == cfg['name']), None) + if match: + diff.append(match) + + for cfg in want: + if cfg['password'] and cfg['update_password'] == 'always': + d_match = next((d_cfg for d_cfg in diff if d_cfg['name'] == cfg['name']), None) + if d_match is None: + diff.append(cfg) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + self.validate_new_users(want, have) + + commands = diff + requests = self.get_modify_users_requests(commands, have) + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the usersi except admin + if not want: + commands = have + else: + commands = want + + requests = self.get_delete_users_requests(commands, have) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_pwd(self, pw): + clear_pwd = hashed_pwd = "" + pwd = pw.replace("\\", "") + if pwd[:3] == '$6$': + hashed_pwd = pwd + else: + clear_pwd = pwd + return clear_pwd, hashed_pwd + + def get_single_user_payload(self, name, role, password, update_pass, match): + user_cfg = {'username': name} + if not role and match: + role = match['role'] + + if not password and match: + password = match['password'] + + if role: + user_cfg['role'] = role + + if password: + clear_pwd, hashed_pwd = self.get_pwd(password) + user_cfg['password'] = clear_pwd + user_cfg['password-hashed'] = hashed_pwd + + pay_load = {'openconfig-system:user': [{'username': name, 'config': user_cfg}]} + return pay_load + + def get_modify_single_user_request(self, conf, match): + request = None + name = conf.get('name', None) + role = conf.get('role', None) + password = conf.get('password', None) + update_pass = conf.get('update_password', None) + if role or (password and update_pass == 'always'): + url = 'data/openconfig-system:system/aaa/authentication/users/user=%s' % (name) + payload = self.get_single_user_payload(name, role, password, update_pass, match) + request = {'path': url, 'method': PATCH, 'data': payload} + return request + + def get_modify_users_requests(self, commands, have): + requests = [] + if not commands: + return requests + + for conf in commands: + match = next((cfg for cfg in have if cfg['name'] == conf['name']), None) + req = self.get_modify_single_user_request(conf, match) + if req: + requests.append(req) + return requests + + def get_new_users(self, want, have): + new_users = [] + for user in want: + if not next((h_user for h_user in have if h_user['name'] == user['name']), None): + new_users.append(user) + return new_users + + def validate_new_users(self, want, have): + new_users = self.get_new_users(want, have) + invalid_users = [] + for user in new_users: + params = [] + if not user['role']: + params.append('role') + if not user['password']: + params.append('password') + if params: + invalid_users.append({user['name']: params}) + if invalid_users: + err_msg = "Missing parameter(s) for new users! " + str(invalid_users) + self._module.fail_json(msg=err_msg, code=513) + + def get_delete_users_requests(self, commands, have): + requests = [] + if not commands: + return requests + + # Skip the asmin user in 'deleted' state. we cannot delete all users + admin_usr = None + + for conf in commands: + # Skip the asmin user in 'deleted' state. we cannot delete all users + if conf['name'] == 'admin': + admin_usr = conf + continue + match = next((cfg for cfg in have if cfg['name'] == conf['name']), None) + if match: + url = 'data/openconfig-system:system/aaa/authentication/users/user=%s' % (conf['name']) + requests.append({'path': url, 'method': DELETE}) + + if admin_usr: + commands.remove(admin_usr) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlans/vlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlans/vlans.py new file mode 100644 index 000000000..404051074 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlans/vlans.py @@ -0,0 +1,265 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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_vlans 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 + +import json + +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, + remove_empties_from_list, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.interfaces_util import ( + build_interfaces_create_request, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils._text import to_native +from ansible.module_utils.connection import ConnectionError +import traceback + +LIB_IMP_ERR = None +ERR_MSG = None +try: + import jinja2 + HAS_LIB = True +except Exception as e: + HAS_LIB = False + ERR_MSG = to_native(e) + LIB_IMP_ERR = traceback.format_exc() + +TEST_KEYS = [ + {'config': {'vlan_id': ''}}, +] + + +class Vlans(ConfigBase): + """ + The sonic_vlans class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'vlans', + ] + + def __init__(self, module): + super(Vlans, self).__init__(module) + + def get_vlans_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) + vlans_facts = facts['ansible_network_resources'].get('vlans') + if not vlans_facts: + return [] + return vlans_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_vlans_facts = self.get_vlans_facts() + commands, requests = self.set_config(existing_vlans_facts) + if commands: + 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_vlans_facts = self.get_vlans_facts() + + result['before'] = existing_vlans_facts + if result['changed']: + result['after'] = changed_vlans_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_vlans_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 = remove_empties_from_list(self._module.params['config']) + have = existing_vlans_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'] + # diff method works on dict, so creating temp dict + diff = get_diff(want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + + ret_commands = remove_empties_from_list(commands) + return ret_commands, requests + + def _state_replaced(self, want, have, diff): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + return self._state_merged(want, have, diff) + + def _state_overridden(self, want, have, diff): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + ret_requests = list() + commands = list() + vlans_to_delete = get_diff(have, want, TEST_KEYS) + if vlans_to_delete: + delete_vlans_requests = self.get_delete_vlans_requests(vlans_to_delete) + ret_requests.extend(delete_vlans_requests) + commands.extend(update_states(vlans_to_delete, "deleted")) + + if diff: + vlans_to_create_requests = self.get_create_vlans_requests(diff) + ret_requests.extend(vlans_to_create_requests) + commands.extend(update_states(diff, "merged")) + + return commands, ret_requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration at position-0 + Requests necessary to merge to the current configuration + at position-1 + """ + commands = update_states(diff, "merged") + requests = self.get_create_vlans_requests(commands) + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = list() + # if want is none, then delete all the vlans + if not want: + commands = have + else: # delete specific vlans + commands = get_diff(want, diff, TEST_KEYS) + + requests = self.get_delete_vlans_requests(commands) + commands = update_states(commands, "deleted") + return commands, requests + + def get_delete_vlans_requests(self, configs): + requests = [] + if not configs: + return requests + # Create URL and payload + url = "data/openconfig-interfaces:interfaces/interface=Vlan{}" + method = "DELETE" + for vlan in configs: + vlan_id = vlan.get("vlan_id") + description = vlan.get("description") + if description: + path = self.get_delete_vlan_config_attr(vlan_id, "description") + else: + path = url.format(vlan_id) + + request = {"path": path, + "method": method, + } + requests.append(request) + + return requests + + def get_delete_vlan_config_attr(self, vlan_id, attr_name): + url = "data/openconfig-interfaces:interfaces/interface=Vlan{}/config/{}" + path = url.format(vlan_id, attr_name) + + return path + + def get_create_vlans_requests(self, configs): + requests = [] + if not configs: + return requests + for vlan in configs: + vlan_id = vlan.get("vlan_id") + interface_name = "Vlan" + str(vlan_id) + description = vlan.get("description", None) + request = build_interfaces_create_request(interface_name=interface_name) + requests.append(request) + if description: + requests.append(self.get_modify_vlan_config_attr(interface_name, 'description', description)) + + return requests + + def get_modify_vlan_config_attr(self, intf_name, attr_name, attr_value): + url = "data/openconfig-interfaces:interfaces/interface={}/config" + payload = {"openconfig-interfaces:config": {"name": intf_name, attr_name: attr_value}} + method = "PATCH" + request = {"path": url.format(intf_name), "method": method, "data": payload} + + return request diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vrfs/vrfs.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vrfs/vrfs.py new file mode 100644 index 000000000..83deb0ecb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vrfs/vrfs.py @@ -0,0 +1,303 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_vrfs 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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states, + normalize_interface_name +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'DELETE' +TEST_KEYS = [ + {'interfaces': {'name': ''}}, +] + + +class Vrfs(ConfigBase): + """ + The sonic_vrfs class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'vrfs', + ] + + delete_all_flag = False + + def __init__(self, module): + super(Vrfs, self).__init__(module) + + def get_vrf_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) + vrf_interfaces_facts = facts['ansible_network_resources'].get('vrfs') + if not vrf_interfaces_facts: + return [] + return vrf_interfaces_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_vrf_interfaces_facts = self.get_vrf_facts() + commands, requests = self.set_config(existing_vrf_interfaces_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_vrf_interfaces_facts = self.get_vrf_facts() + + result['before'] = existing_vrf_interfaces_facts + if result['changed']: + result['after'] = changed_vrf_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_vrf_interfaces_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_vrf_interfaces_facts + if want is None: + want = [] + + for each in want: + if each.get("members", None): + interfaces = each["members"].get("interfaces", None) + if interfaces: + interfaces = normalize_interface_name(interfaces, self._module) + 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'] + diff = get_diff(want, have, TEST_KEYS) + + if state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = [] + if commands: + requests = self.get_create_requests(commands, have) + + if len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + return commands, requests + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :param interface_type: interface type + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the vrfs + if not want: + commands = have + self.delete_all_flag = True + else: + commands = want + self.delete_all_flag = False + + requests = [] + if commands: + requests = self.get_delete_vrf_interface_requests(commands, have, want) + + if len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_delete_vrf_interface_requests(self, configs, have, want): + requests = [] + if not configs: + return requests + + # Create URL and payload + method = DELETE + for conf in configs: + name = conf['name'] + empty_flag = False + members = conf.get('members', None) + if members: + interfaces = members.get('interfaces', None) + if members is None: + empty_flag = True + elif members is not None and interfaces is None: + empty_flag = True + matched = next((have_cfg for have_cfg in have if have_cfg['name'] == name), None) + if not matched: + continue + + # if members are not mentioned delet the vrf name + if (self._module.params['state'] == 'deleted' and self.delete_all_flag) or empty_flag: + url = 'data/openconfig-network-instance:network-instances/network-instance={0}'.format(name) + request = {"path": url, "method": method} + requests.append(request) + else: + matched_members = matched.get('members', None) + + if matched_members: + matched_intf = matched_members.get('interfaces', None) + if matched_intf: + for del_mem in matched_intf: + url = 'data/openconfig-network-instance:network-instances/' + url = url + 'network-instance={0}/interfaces/interface={1}'.format(name, del_mem['name']) + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_create_requests(self, configs, have): + requests = [] + if not configs: + return requests + + requests_vrf = self.get_create_vrf_requests(configs, have) + if requests_vrf: + requests.extend(requests_vrf) + + requests_vrf_intf = self.get_create_vrf_interface_requests(configs, have) + if requests_vrf_intf: + requests.extend(requests_vrf_intf) + return requests + + def get_create_vrf_requests(self, configs, have): + requests = [] + if not configs: + return requests + # Create URL and payload + method = PATCH + for conf in configs: + if conf.get("name", None): + name = conf["name"] + matched = next((have_cfg for have_cfg in have if have_cfg['name'] == name), None) + if not matched: + url = 'data/openconfig-network-instance:network-instances' + payload = self.build_create_vrf_payload(conf) + request = {"path": url, "method": method, "data": payload} + requests.append(request) + return requests + + def get_create_vrf_interface_requests(self, configs, have): + requests = [] + if not configs: + return requests + + # Create URL and payload + method = PATCH + for conf in configs: + if conf.get("members", None): + if conf["members"].get("interfaces", None): + url = 'data/openconfig-network-instance:network-instances/network-instance={0}/interfaces/interface'.format(conf["name"]) + payload = self.build_create_vrf_interface_payload(conf) + if payload: + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def build_create_vrf_payload(self, conf): + name = conf['name'] + + netw_inst = dict({'name': name}) + netw_inst['config'] = dict({'name': name}) + netw_inst['config'].update({'enabled': True}) + netw_inst['config'].update({'type': 'L3VRF'}) + netw_inst_arr = [netw_inst] + + return dict({'openconfig-network-instance:network-instances': {'network-instance': netw_inst_arr}}) + + def build_create_vrf_interface_payload(self, conf): + members = conf["members"].get("interfaces", None) + network_inst_payload = dict() + if members: + network_inst_payload.update({"openconfig-network-instance:interface": []}) + for member in members: + if member["name"]: + member_config_payload = dict({"id": member["name"]}) + member_payload = dict({"id": member["name"], "config": member_config_payload}) + network_inst_payload["openconfig-network-instance:interface"].append(member_payload) + + return network_inst_payload diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vxlans/vxlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vxlans/vxlans.py new file mode 100644 index 000000000..d44adcedf --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vxlans/vxlans.py @@ -0,0 +1,606 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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_vxlans 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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'patch' +DELETE = 'delete' +test_keys = [ + {'vlan_map': {'vlan': '', 'vni': ''}}, + {'vrf_map': {'vni': '', 'vrf': ''}}, +] + + +class Vxlans(ConfigBase): + """ + The sonic_vxlans class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'vxlans', + ] + + def __init__(self, module): + super(Vxlans, self).__init__(module) + + def get_vxlans_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) + vxlans_facts = facts['ansible_network_resources'].get('vxlans') + if not vxlans_facts: + return [] + return vxlans_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_vxlans_facts = self.get_vxlans_facts() + commands, requests = self.set_config(existing_vxlans_facts) + + if commands and requests: + 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_vxlans_facts = self.get_vxlans_facts() + + result['before'] = existing_vxlans_facts + if result['changed']: + result['after'] = changed_vxlans_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_vxlans_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_vxlans_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'] + + diff = get_diff(want, have, test_keys) + + if state == 'overridden': + commands, requests = self._state_overridden(want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) + + return commands, requests + + def _state_replaced(self, want, have, diff): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + + requests = [] + commands = [] + + commands_del = get_diff(have, want, test_keys) + requests_del = [] + if commands_del: + requests_del = self.get_delete_vxlan_request(commands_del, have) + if requests_del: + requests.extend(requests_del) + commands_del = update_states(commands_del, "deleted") + commands.extend(commands_del) + + commands_rep = diff + requests_rep = [] + if commands_rep: + requests_rep = self.get_create_vxlans_request(commands_rep, have) + if requests_rep: + requests.extend(requests_rep) + commands_rep = update_states(commands_rep, "replaced") + commands.extend(commands_rep) + + return commands, requests + + def _state_overridden(self, want, have, diff): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + requests = [] + commands = [] + + commands_del = get_diff(have, want) + requests_del = [] + if commands_del: + requests_del = self.get_delete_vxlan_request(commands_del, have) + if requests_del: + requests.extend(requests_del) + commands_del = update_states(commands_del, "deleted") + commands.extend(commands_del) + + commands_over = diff + requests_over = [] + if commands_over: + requests_over = self.get_create_vxlans_request(commands_over, have) + if requests_over: + requests.extend(requests_over) + commands_over = update_states(commands_over, "overridden") + commands.extend(commands_over) + + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration at position-0 + Requests necessary to merge to the current configuration + at position-1 + """ + commands = diff + requests = self.get_create_vxlans_request(commands, have) + + if len(requests) == 0: + commands = [] + + if commands: + commands = update_states(commands, "merged") + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + + requests = [] + is_delete_all = False + # if want is none, then delete all the vxlans + if not want or len(have) == 0: + commands = have + is_delete_all = True + else: + commands = want + + if is_delete_all: + requests = self.get_delete_all_vxlan_request(have) + else: + requests = self.get_delete_vxlan_request(commands, have) + + if len(requests) == 0: + commands = [] + + if commands: + commands = update_states(commands, "deleted") + + return commands, requests + + def get_create_vxlans_request(self, configs, have): + requests = [] + + if not configs: + return requests + + tunnel_requests = self.get_create_tunnel_request(configs, have) + vlan_map_requests = self.get_create_vlan_map_request(configs, have) + vrf_map_requests = self.get_create_vrf_map_request(configs, have) + + if tunnel_requests: + requests.extend(tunnel_requests) + if vlan_map_requests: + requests.extend(vlan_map_requests) + if vrf_map_requests: + requests.extend(vrf_map_requests) + + return requests + + def get_delete_all_vxlan_request(self, have): + requests = [] + + vrf_map_requests = [] + vlan_map_requests = [] + src_ip_requests = [] + primary_ip_requests = [] + tunnel_requests = [] + + # Need to delete in reverse order of creation. + # vrf_map needs to be cleared before vlan_map + # vlan_map needs to be cleared before tunnel(source-ip) + for conf in have: + name = conf['name'] + vlan_map_list = conf.get('vlan_map', []) + vrf_map_list = conf.get('vrf_map', []) + src_ip = conf.get('source_ip', None) + primary_ip = conf.get('primary_ip', None) + + if vrf_map_list: + vrf_map_requests.extend(self.get_delete_vrf_map_request(conf, conf, name, vrf_map_list)) + if vlan_map_list: + vlan_map_requests.extend(self.get_delete_vlan_map_request(conf, conf, name, vlan_map_list)) + if src_ip: + src_ip_requests.extend(self.get_delete_src_ip_request(conf, conf, name, src_ip)) + if primary_ip: + primary_ip_requests.extend(self.get_delete_primary_ip_request(conf, conf, name, primary_ip)) + tunnel_requests.extend(self.get_delete_tunnel_request(conf, conf, name)) + + if vrf_map_requests: + requests.extend(vrf_map_requests) + if vlan_map_requests: + requests.extend(vlan_map_requests) + if src_ip_requests: + requests.extend(src_ip_requests) + if primary_ip_requests: + requests.extend(primary_ip_requests) + if tunnel_requests: + requests.extend(tunnel_requests) + + return requests + + def get_delete_vxlan_request(self, configs, have): + requests = [] + + if not configs: + return requests + + vrf_map_requests = [] + vlan_map_requests = [] + src_ip_requests = [] + primary_ip_requests = [] + tunnel_requests = [] + + # Need to delete in the reverse order of creation. + # vrf_map needs to be cleared before vlan_map + # vlan_map needs to be cleared before tunnel(source-ip) + for conf in configs: + + name = conf['name'] + src_ip = conf.get('source_ip', None) + primary_ip = conf.get('primary_ip', None) + vlan_map_list = conf.get('vlan_map', None) + vrf_map_list = conf.get('vrf_map', None) + + have_vlan_map_count = 0 + have_vrf_map_count = 0 + matched = next((each_vxlan for each_vxlan in have if each_vxlan['name'] == name), None) + if matched: + have_vlan_map = matched.get('vlan_map', []) + have_vrf_map = matched.get('vrf_map', []) + if have_vlan_map: + have_vlan_map_count = len(have_vlan_map) + if have_vrf_map: + have_vrf_map_count = len(have_vrf_map) + + is_delete_full = False + if (name and vlan_map_list is None and vrf_map_list is None and + src_ip is None and primary_ip is None): + is_delete_full = True + vrf_map_list = matched.get("vrf_map", []) + vlan_map_list = matched.get("vlan_map", []) + + if vlan_map_list is not None and len(vlan_map_list) == 0 and matched: + vlan_map_list = matched.get("vlan_map", []) + if vrf_map_list is not None and len(vrf_map_list) == 0 and matched: + vrf_map_list = matched.get("vrf_map", []) + + if vrf_map_list: + temp_vrf_map_requests = self.get_delete_vrf_map_request(conf, matched, name, vrf_map_list) + if temp_vrf_map_requests: + vrf_map_requests.extend(temp_vrf_map_requests) + have_vrf_map_count -= len(temp_vrf_map_requests) + if vlan_map_list: + temp_vlan_map_requests = self.get_delete_vlan_map_request(conf, matched, name, vlan_map_list) + if temp_vlan_map_requests: + vlan_map_requests.extend(temp_vlan_map_requests) + have_vlan_map_count -= len(temp_vlan_map_requests) + if src_ip: + src_ip_requests.extend(self.get_delete_src_ip_request(conf, matched, name, src_ip)) + + if primary_ip: + primary_ip_requests.extend(self.get_delete_primary_ip_request(conf, matched, name, primary_ip)) + if is_delete_full: + tunnel_requests.extend(self.get_delete_tunnel_request(conf, matched, name)) + + if vrf_map_requests: + requests.extend(vrf_map_requests) + if vlan_map_requests: + requests.extend(vlan_map_requests) + if src_ip_requests: + requests.extend(src_ip_requests) + if primary_ip_requests: + requests.extend(primary_ip_requests) + if tunnel_requests: + requests.extend(tunnel_requests) + + return requests + + def get_create_evpn_request(self, conf): + # Create URL and payload + url = "data/sonic-vxlan:sonic-vxlan/EVPN_NVO/EVPN_NVO_LIST" + payload = self.build_create_evpn_payload(conf) + request = {"path": url, "method": PATCH, "data": payload} + + return request + + def get_create_tunnel_request(self, configs, have): + # Create URL and payload + requests = [] + url = "data/sonic-vxlan:sonic-vxlan/VXLAN_TUNNEL" + for conf in configs: + payload = self.build_create_tunnel_payload(conf) + request = {"path": url, "method": PATCH, "data": payload} + requests.append(request) + if conf.get('source_ip', None): + requests.append(self.get_create_evpn_request(conf)) + + return requests + + def build_create_evpn_payload(self, conf): + + evpn_nvo_list = [{'name': conf['evpn_nvo'], 'source_vtep': conf['name']}] + evpn_dict = {'sonic-vxlan:EVPN_NVO_LIST': evpn_nvo_list} + + return evpn_dict + + def build_create_tunnel_payload(self, conf): + payload_url = dict() + + vtep_ip_dict = dict() + vtep_ip_dict['name'] = conf['name'] + if conf.get('source_ip', None): + vtep_ip_dict['src_ip'] = conf['source_ip'] + if conf.get('primary_ip', None): + vtep_ip_dict['primary_ip'] = conf['primary_ip'] + + payload_url['sonic-vxlan:VXLAN_TUNNEL'] = {'VXLAN_TUNNEL_LIST': [vtep_ip_dict]} + + return payload_url + + def get_create_vlan_map_request(self, configs, have): + # Create URL and payload + requests = [] + for conf in configs: + new_vlan_map_list = conf.get('vlan_map', []) + if new_vlan_map_list: + for each_vlan_map in new_vlan_map_list: + name = conf['name'] + vlan = each_vlan_map.get('vlan') + vni = each_vlan_map.get('vni') + matched = next((each_vxlan for each_vxlan in have if each_vxlan['name'] == name), None) + + is_change_needed = True + if matched: + matched_vlan_map_list = matched.get('vlan_map', []) + if matched_vlan_map_list: + matched_vlan_map = next((e_vlan_map for e_vlan_map in matched_vlan_map_list if e_vlan_map['vni'] == vni), None) + if matched_vlan_map: + if matched_vlan_map['vlan'] == vlan: + is_change_needed = False + + if is_change_needed: + map_name = "map_{0}_Vlan{1}".format(vni, vlan) + payload = self.build_create_vlan_map_payload(conf, each_vlan_map) + url = "data/sonic-vxlan:sonic-vxlan/VXLAN_TUNNEL_MAP" + request = {"path": url, "method": PATCH, "data": payload} + requests.append(request) + + return requests + + def build_create_vlan_map_payload(self, conf, vlan_map): + payload_url = dict() + + vlan_map_dict = dict() + vlan_map_dict['name'] = conf['name'] + vlan_map_dict['mapname'] = "map_{vni}_Vlan{vlan}".format(vni=vlan_map['vni'], vlan=vlan_map['vlan']) + vlan_map_dict['vlan'] = "Vlan{vlan}".format(vlan=vlan_map['vlan']) + vlan_map_dict['vni'] = vlan_map['vni'] + + payload_url['sonic-vxlan:VXLAN_TUNNEL_MAP'] = {'VXLAN_TUNNEL_MAP_LIST': [vlan_map_dict]} + + return payload_url + + def get_create_vrf_map_request(self, configs, have): + # Create URL and payload + requests = [] + for conf in configs: + new_vrf_map_list = conf.get('vrf_map', []) + if new_vrf_map_list: + for each_vrf_map in new_vrf_map_list: + name = conf['name'] + vrf = each_vrf_map.get('vrf') + vni = each_vrf_map.get('vni') + matched = next((each_vxlan for each_vxlan in have if each_vxlan['name'] == name), None) + + is_change_needed = True + if matched: + matched_vrf_map_list = matched.get('vrf_map', []) + if matched_vrf_map_list: + matched_vrf_map = next((e_vrf_map for e_vrf_map in matched_vrf_map_list if e_vrf_map['vni'] == vni), None) + if matched_vrf_map: + if matched_vrf_map['vrf'] == vrf: + is_change_needed = False + + if is_change_needed: + payload = self.build_create_vrf_map_payload(conf, each_vrf_map) + url = "data/sonic-vrf:sonic-vrf/VRF/VRF_LIST={vrf}/vni".format(vrf=vrf) + request = {"path": url, "method": PATCH, "data": payload} + requests.append(request) + + return requests + + def build_create_vrf_map_payload(self, conf, vrf_map): + + payload_url = dict({"sonic-vrf:vni": vrf_map['vni']}) + return payload_url + + def get_delete_evpn_request(self, conf): + # Create URL and payload + url = "data/sonic-vxlan:sonic-vxlan/EVPN_NVO/EVPN_NVO_LIST={evpn_nvo}".format(evpn_nvo=conf['evpn_nvo']) + request = {"path": url, "method": DELETE} + + return request + + def get_delete_tunnel_request(self, conf, matched, name): + # Create URL and payload + requests = [] + + url = "data/sonic-vxlan:sonic-vxlan/VXLAN_TUNNEL/VXLAN_TUNNEL_LIST={name}".format(name=name) + requests.append({"path": url, "method": DELETE}) + + return requests + + def get_delete_src_ip_request(self, conf, matched, name, del_source_ip): + # Create URL and payload + requests = [] + + url = "data/sonic-vxlan:sonic-vxlan/VXLAN_TUNNEL/VXLAN_TUNNEL_LIST={name}/src_ip" + + is_change_needed = False + if matched: + matched_source_ip = matched.get('source_ip', None) + if matched_source_ip and matched_source_ip == del_source_ip: + is_change_needed = True + + # Delete the EVPN NVO if the source_ip address is being deleted. + if is_change_needed: + requests.append(self.get_delete_evpn_request(conf)) + request = {"path": url.format(name=name), "method": DELETE} + requests.append(request) + + return requests + + def get_delete_primary_ip_request(self, conf, matched, name, del_primary_ip): + # Create URL and payload + requests = [] + + url = "data/sonic-vxlan:sonic-vxlan/VXLAN_TUNNEL/VXLAN_TUNNEL_LIST={name}/primary_ip" + + is_change_needed = False + if matched: + matched_primary_ip = matched.get('primary_ip', None) + if matched_primary_ip and matched_primary_ip == del_primary_ip: + is_change_needed = True + + if is_change_needed: + request = {"path": url.format(name=name), "method": DELETE} + requests.append(request) + + return requests + + def get_delete_vlan_map_request(self, conf, matched, name, del_vlan_map_list): + # Create URL and payload + requests = [] + + for each_vlan_map in del_vlan_map_list: + vlan = each_vlan_map.get('vlan') + vni = each_vlan_map.get('vni') + + is_change_needed = False + if matched: + matched_vlan_map_list = matched.get('vlan_map', None) + if matched_vlan_map_list: + matched_vlan_map = next((e_vlan_map for e_vlan_map in matched_vlan_map_list if e_vlan_map['vni'] == vni), None) + if matched_vlan_map: + if matched_vlan_map['vlan'] == vlan: + is_change_needed = True + + if is_change_needed: + map_name = "map_{0}_Vlan{1}".format(vni, vlan) + url = "data/sonic-vxlan:sonic-vxlan/VXLAN_TUNNEL_MAP/VXLAN_TUNNEL_MAP_LIST={name},{map_name}".format(name=name, map_name=map_name) + request = {"path": url, "method": DELETE} + requests.append(request) + + return requests + + def get_delete_vrf_map_request(self, conf, matched, name, del_vrf_map_list): + # Create URL and payload + requests = [] + + for each_vrf_map in del_vrf_map_list: + vrf = each_vrf_map.get('vrf') + vni = each_vrf_map.get('vni') + + is_change_needed = False + if matched: + matched_vrf_map_list = matched.get('vrf_map', None) + if matched_vrf_map_list: + matched_vrf_map = next((e_vrf_map for e_vrf_map in matched_vrf_map_list if e_vrf_map['vni'] == vni), None) + if matched_vrf_map: + if matched_vrf_map['vrf'] == vrf: + is_change_needed = True + + if is_change_needed: + url = "data/sonic-vrf:sonic-vrf/VRF/VRF_LIST={vrf}/vni".format(vrf=vrf) + request = {"path": url, "method": DELETE} + requests.append(request) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/aaa/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/aaa/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/aaa/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/aaa/aaa.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/aaa/aaa.py new file mode 100644 index 000000000..5a7bd05c9 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/aaa/aaa.py @@ -0,0 +1,111 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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 aaa 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 + +import re +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.aaa.aaa import AaaArgs + +GET = "get" + + +class AaaFacts(object): + """ The sonic aaa fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = AaaArgs.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 get_aaa(self): + """Get aaa details available in chassis""" + request = [{"path": "data/openconfig-system:system/aaa", "method": GET}] + 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) + data = {} + if ('openconfig-system:aaa' in response[0][1]): + if ('authentication' in response[0][1]['openconfig-system:aaa']): + if ('config' in response[0][1]['openconfig-system:aaa']['authentication']): + data = response[0][1]['openconfig-system:aaa']['authentication']['config'] + return data + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for aaa + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_aaa() + objs = [] + objs = self.render_config(self.generated_spec, data) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['aaa'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = self.parse_sonic_aaa(spec, conf) + return config + + def parse_sonic_aaa(self, spec, conf): + config = deepcopy(spec) + if conf: + temp = {} + if ('authentication-method' in conf) and (conf['authentication-method']): + if 'local' in conf['authentication-method']: + temp['local'] = True + choices = ['tacacs+', 'ldap', 'radius'] + for i, word in enumerate(conf['authentication-method']): + if word in choices: + temp['group'] = conf['authentication-method'][i] + if ('failthrough' in conf): + temp['fail_through'] = conf['failthrough'] + if temp: + config['authentication']['data'] = temp + return utils.remove_empties(config) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp/bgp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp/bgp.py new file mode 100644 index 000000000..c86b53c2a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp/bgp.py @@ -0,0 +1,156 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic bgp 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 + +import re +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.utils.utils import ( + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.bgp.bgp import BgpArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( + get_bgp_data, +) + + +class BgpFacts(object): + """ The sonic bgp fact class + """ + + global_params_map = { + 'bgp_as': 'as', + 'router_id': 'router-id', + 'holdtime': 'hold-time', + 'keepalive_interval': 'keepalive-interval', + 'log_neighbor_changes': ['logging-options', 'log-neighbor-state-changes'], + 'as_path_confed': ['route-selection-options', 'compare-confed-as-path'], + 'as_path_ignore': ['route-selection-options', 'ignore-as-path-length'], + 'as_path_multipath_relax': ['use-multiple-paths', 'ebgp', 'config', 'allow-multiple-as'], + 'as_path_multipath_relax_as_set': ['use-multiple-paths', 'ebgp', 'config', 'as-set'], + 'compare_routerid': ['route-selection-options', 'external-compare-router-id'], + 'med_confed': ['route-selection-options', 'med-confed'], + 'med_missing_as_worst': ['route-selection-options', 'med-missing-as-worst'], + 'always_compare_med': ['route-selection-options', 'always-compare-med'], + 'admin_max_med': ['max-med', 'admin-max-med-val'], + 'max_med_on_startup_timer': ['max-med', 'time'], + 'max_med_on_startup_med_val': ['max-med', 'max-med-val'], + } + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = BgpArgs.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 BGP + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = list() + if connection: # just for linting purposes, remove + pass + + if not data: + data = get_bgp_data(self._module, self.global_params_map) + self.normalise_bgp_data(data) + + # operate on a collection of resource x + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + # split the config into instances of the resource + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bgp', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': remove_empties_from_list(objs)}) + facts['bgp'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def normalise_bgp_data(self, data): + for conf in data: + bestpath = {} + med = {} + timers = {} + as_path = {} + max_med_on_start_up = {} + + conf['log_neighbor_changes'] = conf.get('log_neighbor_changes', False) + + as_path['confed'] = conf.get('as_path_confed', False) + as_path['ignore'] = conf.get('as_path_ignore', False) + as_path['multipath_relax'] = conf.get('as_path_multipath_relax', False) + as_path['multipath_relax_as_set'] = conf.get('as_path_multipath_relax_as_set', False) + bestpath['as_path'] = as_path + + med['confed'] = conf.get('med_confed', False) + med['missing_as_worst'] = conf.get('med_missing_as_worst', False) + med['always_compare_med'] = conf.get('always_compare_med', False) + bestpath['med'] = med + + timers['holdtime'] = conf.get('holdtime', None) + timers['keepalive_interval'] = conf.get('keepalive_interval', None) + conf['timers'] = timers + bestpath['compare_routerid'] = conf.get('compare_routerid', False) + + conf['bestpath'] = bestpath + + max_med_on_start_up["timer"] = conf.get('max_med_on_startup_timer', None) + max_med_on_start_up["med_val"] = conf.get('max_med_on_startup_med_val', None) + + conf['max_med'] = { + 'on_startup': max_med_on_start_up, + } + + keys = [ + 'as_path_confed', 'as_path_ignore', 'as_path_multipath_relax', 'as_path_multipath_relax_as_set', + 'med_confed', 'med_missing_as_worst', 'always_compare_med', 'max_med_val', 'holdtime', + 'keepalive_interval', 'compare_routerid', 'admin_max_med', 'max_med_on_startup_timer', + 'max_med_on_startup_med_val', + ] + for key in keys: + if key in conf: + conf.pop(key) + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + + return conf diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_af/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_af/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_af/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_af/bgp_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_af/bgp_af.py new file mode 100644 index 000000000..fd37533e4 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_af/bgp_af.py @@ -0,0 +1,258 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic bgp_af 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 + +import re +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.utils.utils import ( + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.bgp_af.bgp_af import Bgp_afArgs + +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( + get_bgp_af_data, + get_all_bgp_af_redistribute, +) + + +class Bgp_afFacts(object): + """ The sonic bgp_af fact class + """ + + afi_safi_types_map = { + 'openconfig-bgp-types:IPV4_UNICAST': 'ipv4_unicast', + 'openconfig-bgp-types:IPV6_UNICAST': 'ipv6_unicast', + 'openconfig-bgp-types:L2VPN_EVPN': 'l2vpn_evpn', + } + + af_params_map = { + 'afi': 'afi-safi-name', + 'route_map': 'policy-name', + 'prefix': 'prefix', + 'neighbor': 'neighbor-address', + 'route_reflector_client': 'route-reflector-client', + 'route_server_client': 'route-server-client', + 'next_hop_self': ['next-hop-self', 'enabled'], + 'remove_private_as': ['remove-private-as', 'enabled'], + 'prefix_list_in': ['prefix-list', 'import-policy'], + 'prefix_list_out': ['prefix-list', 'export-policy'], + 'maximum_prefix': ['prefix-limit', 'max-prefixes'], + 'activate': 'enabled', + 'advertise_pip': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'advertise-pip'], + 'advertise_pip_ip': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'advertise-pip-ip'], + 'advertise_pip_peer_ip': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'advertise-pip-peer-ip'], + 'advertise_svi_ip': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'advertise-svi-ip'], + 'advertise_all_vni': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'advertise-all-vni'], + 'advertise_default_gw': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'advertise-default-gw'], + 'ebgp': ['use-multiple-paths', 'ebgp', 'maximum-paths'], + 'ibgp': ['use-multiple-paths', 'ibgp', 'maximum-paths'], + 'network': ['network-config', 'network'], + 'dampening': ['route-flap-damping', 'config', 'enabled'], + 'route_advertise_list': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:route-advertise', 'route-advertise-list'], + } + + af_redis_params_map = { + 'protocol': 'src-protocol', + 'afi': 'address-family', + 'metric': 'metric', + 'route_map': 'import-policy' + } + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Bgp_afArgs.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 BGP + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = list() + if connection: # just for linting purposes, remove + pass + if not data: + data = get_bgp_af_data(self._module, self.af_params_map) + vrf_list = [e_bgp_af['vrf_name'] for e_bgp_af in data] + self.update_max_paths(data) + self.update_network(data) + self.update_route_advertise_list(data) + bgp_redis_data = get_all_bgp_af_redistribute(self._module, vrf_list, self.af_redis_params_map) + self.update_redis_data(data, bgp_redis_data) + self.update_afis(data) + + # operate on a collection of resource x + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + # split the config into instances of the resource + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bgp_af', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': remove_empties_from_list(objs)}) + facts['bgp_af'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + + return conf + + def check_afi(self, afi, redis_data): + afi_rhs = afi + afi_lhs = redis_data.get('afi', None) + return (afi_lhs and (afi_rhs == afi_lhs)) + + def update_redis_data(self, objs, af_redis_data): + if not (af_redis_data or objs): + return + + for conf in objs: + vrf_name = conf['vrf_name'] + raw_af_redis_data = next((e_af_redis for e_af_redis in af_redis_data if vrf_name in e_af_redis), None) + if not raw_af_redis_data: + continue + norm_af_redis_data = self.normalize_af_redis_params(raw_af_redis_data[vrf_name]) + if norm_af_redis_data: + if 'address_family' in conf: + afs = conf['address_family'] + if not afs: + continue + for e_af in afs: + if 'afi' in e_af: + afi = e_af['afi'] + redis_arr = [] + for e_redis_data in norm_af_redis_data: + if self.check_afi(afi, e_redis_data): + e_redis_data.pop('afi') + redis_arr.append(e_redis_data) + e_af.update({'redistribute': redis_arr}) + else: + addr_fams = [] + for e_norm_af_redis in norm_af_redis_data: + afi = e_norm_af_redis['afi'] + e_norm_af_redis.pop('afi') + mat_addr_fam = next((each_addr_fam for each_addr_fam in addr_fams if each_addr_fam['afi'] == afi), None) + if mat_addr_fam: + mat_addr_fam['redistribute'].append(e_norm_af_redis) + else: + addr_fams.append({'redistribute': [e_norm_af_redis], 'afi': afi}) + + if addr_fams: + conf.update({'address_family': addr_fams}) + + def update_max_paths(self, data): + for conf in data: + afs = conf.get('address_family', []) + if afs: + for af in afs: + max_path = {} + ebgp = af.get('ebgp', None) + if ebgp: + af.pop('ebgp') + max_path['ebgp'] = ebgp + ibgp = af.get('ibgp', None) + if ibgp: + af.pop('ibgp') + max_path['ibgp'] = ibgp + if max_path: + af['max_path'] = max_path + + def update_network(self, data): + for conf in data: + afs = conf.get('address_family', []) + if afs: + for af in afs: + temp = [] + network = af.get('network', None) + if network: + for e in network: + prefix = e.get('prefix', None) + if prefix: + temp.append(prefix) + af['network'] = temp + dampening = af.get('dampening', None) + if dampening: + af.pop('dampening') + af['dampening'] = dampening + + def update_afis(self, data): + for conf in data: + if 'address_family' in conf: + conf['address_family'] = {'afis': conf['address_family']} + + def update_route_advertise_list(self, data): + for conf in data: + afs = conf.get('address_family', []) + if afs: + for af in afs: + rt_adv_lst = [] + route_advertise_list = af.get('route_advertise_list', None) + if route_advertise_list: + for rt in route_advertise_list: + rt_adv_dict = {} + advertise_afi = rt['advertise-afi-safi'].split(':')[1].split('_')[0].lower() + route_map_config = rt['config'] + route_map = route_map_config.get('route-map', None) + if advertise_afi: + rt_adv_dict['advertise_afi'] = advertise_afi + if route_map: + rt_adv_dict['route_map'] = route_map[0] + if rt_adv_dict and rt_adv_dict not in rt_adv_lst: + rt_adv_lst.append(rt_adv_dict) + af['route_advertise_list'] = rt_adv_lst + + def normalize_af_redis_params(self, af): + norm_af = list() + for e_af in af: + temp = e_af.copy() + for key, val in e_af.items(): + if 'afi' == key or 'protocol' == key and val: + if ':' in val: + temp[key] = val.split(':')[1].lower() + if '_' in val: + temp[key] = val.split('_')[1].lower() + elif 'route_map' == key and val: + temp['route_map'] = val[0] + + norm_af.append(temp) + return norm_af diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_as_paths/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_as_paths/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_as_paths/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_as_paths/bgp_as_paths.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_as_paths/bgp_as_paths.py new file mode 100644 index 000000000..822db22a4 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_as_paths/bgp_as_paths.py @@ -0,0 +1,129 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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 bgp_as_paths 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 +import re +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.bgp_as_paths.bgp_as_paths import Bgp_as_pathsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + + +class Bgp_as_pathsFacts(object): + """ The sonic bgp_as_paths fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Bgp_as_pathsArgs.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 get_as_path_list(self): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/as-path-sets" + method = "GET" + request = [{"path": url, "method": method}] + + 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) + + as_path_lists = [] + if "openconfig-bgp-policy:as-path-sets" in response[0][1]: + temp = response[0][1].get("openconfig-bgp-policy:as-path-sets", {}) + if "as-path-set" in temp: + as_path_lists = temp["as-path-set"] + + as_path_list_configs = [] + for as_path in as_path_lists: + result = dict() + as_name = as_path["as-path-set-name"] + member_config = as_path['config'] + members = member_config.get("as-path-set-member", []) + permit_str = member_config.get("openconfig-bgp-policy-ext:action", None) + result['name'] = as_name + result['members'] = members + if permit_str and permit_str == "PERMIT": + result['permit'] = True + else: + result['permit'] = False + as_path_list_configs.append(result) + # with open('/root/ansible_log.log', 'a+') as fp: + # fp.write('as_path_list: ' + str(as_path_list_configs) + '\n') + return as_path_list_configs + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for as_path_list + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + resources = self.get_as_path_list() + + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bgp_as_paths', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['bgp_as_paths'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + try: + config['name'] = str(conf['name']) + config['members'] = conf['members'] + config['permit'] = conf['permit'] + except TypeError: + config['name'] = None + config['members'] = None + config['permit'] = None + return utils.remove_empties(config) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_communities/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_communities/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_communities/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_communities/bgp_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_communities/bgp_communities.py new file mode 100644 index 000000000..ffa294221 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_communities/bgp_communities.py @@ -0,0 +1,145 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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 bgp_communities 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 +import re +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.bgp_communities.bgp_communities import Bgp_communitiesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + + +class Bgp_communitiesFacts(object): + """ The sonic bgp_communities fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Bgp_communitiesArgs.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 get_bgp_communities(self): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/community-sets" + method = "GET" + request = [{"path": url, "method": method}] + + 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) + + bgp_communities = [] + if "openconfig-bgp-policy:community-sets" in response[0][1]: + temp = response[0][1].get("openconfig-bgp-policy:community-sets", {}) + if "community-set" in temp: + bgp_communities = temp["community-set"] + + bgp_communities_configs = [] + for bgp_community in bgp_communities: + result = dict() + name = bgp_community["community-set-name"] + member_config = bgp_community['config'] + match = member_config['match-set-options'] + permit_str = member_config.get('openconfig-bgp-policy-ext:action', None) + members = member_config.get("community-member", []) + result['name'] = name + result['match'] = match + if permit_str and permit_str == 'PERMIT': + result['permit'] = True + else: + result['permit'] = False + if members: + result['type'] = 'expanded' if 'REGEX' in members[0] else 'standard' + else: + result['type'] = '' + if result['type'] == 'expanded': + members = [':'.join(i.split(':')[1:]) for i in members] + result['local_as'] = True if "NO_EXPORT_SUBCONFED" in members else False + result['no_advertise'] = True if "NO_ADVERTISE" in members else False + result['no_export'] = True if "NO_EXPORT" in members else False + result['no_peer'] = True if "NOPEER" in members else False + result['members'] = {'regex': members} + bgp_communities_configs.append(result) + # with open('/root/ansible_log.log', 'a+') as fp: + # fp.write('bgp_communities: ' + str(bgp_communities_configs) + '\n') + return bgp_communities_configs + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for bgp_communities + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + resources = self.get_bgp_communities() + + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bgp_communities', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['bgp_communities'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + try: + config['name'] = str(conf['name']) + config['members'] = conf['members'] + config['match'] = conf['match'] + config['type'] = conf['type'] + config['permit'] = conf['permit'] + except TypeError: + config['name'] = None + config['members'] = None + config['match'] = None + config['type'] = None + config['permit'] = None + return utils.remove_empties(config) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_ext_communities/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_ext_communities/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_ext_communities/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_ext_communities/bgp_ext_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_ext_communities/bgp_ext_communities.py new file mode 100644 index 000000000..b1d7c4ad0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_ext_communities/bgp_ext_communities.py @@ -0,0 +1,158 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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 bgp_ext_communities 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 +import re +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.bgp_ext_communities.bgp_ext_communities import ( + Bgp_ext_communitiesArgs, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + + +class Bgp_ext_communitiesFacts(object): + """ The sonic bgp_ext_communities fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Bgp_ext_communitiesArgs.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 get_bgp_extcommunities(self): + url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:bgp-defined-sets/ext-community-sets" + method = "GET" + request = [{"path": url, "method": method}] + + 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) + + bgp_extcommunities = [] + if "openconfig-bgp-policy:ext-community-sets" in response[0][1]: + temp = response[0][1].get("openconfig-bgp-policy:ext-community-sets", {}) + if "ext-community-set" in temp: + bgp_extcommunities = temp["ext-community-set"] + + bgp_extcommunities_configs = [] + for bgp_extcommunity in bgp_extcommunities: + result = dict() + name = bgp_extcommunity["ext-community-set-name"] + member_config = bgp_extcommunity['config'] + match = member_config['match-set-options'] + permit_str = member_config.get('openconfig-bgp-policy-ext:action', None) + members = member_config.get("ext-community-member", []) + result['name'] = name + result['match'] = match.lower() + + if permit_str and permit_str == 'PERMIT': + result['permit'] = True + else: + result['permit'] = False + + result['members'] = dict() + rt = list() + soo = list() + regex = list() + for member in members: + if member.startswith('route-target'): + rt.append(':'.join(member.split(':')[1:])) + elif member.startswith('route-origin'): + soo.append(':'.join(member.split(':')[1:])) + elif member.startswith('REGEX'): + regex.append(':'.join(member.split(':')[1:])) + + result['type'] = 'standard' + if regex and len(regex) > 0: + result['type'] = 'expanded' + result['members']['regex'] = regex + if rt and len(rt) > 0: + result['members']['route_target'] = rt + if soo and len(soo) > 0: + result['members']['route_origin'] = soo + + bgp_extcommunities_configs.append(result) + + return bgp_extcommunities_configs + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for bgp_ext_communities + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + resources = self.get_bgp_extcommunities() + + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bgp_ext_communities', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['bgp_ext_communities'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + try: + config['name'] = str(conf['name']) + config['members'] = conf['members'] + config['match'] = conf['match'] + config['type'] = conf['type'] + config['permit'] = conf['permit'] + except TypeError: + config['name'] = None + config['members'] = None + config['match'] = None + config['type'] = None + config['permit'] = None + return utils.remove_empties(config) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors/bgp_neighbors.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors/bgp_neighbors.py new file mode 100644 index 000000000..903b93de1 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors/bgp_neighbors.py @@ -0,0 +1,229 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic bgp_neighbors 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 + +import re +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.utils.utils import ( + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.bgp_neighbors.bgp_neighbors import Bgp_neighborsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( + get_all_bgp_neighbors, + get_from_params_map, + get_peergroups, +) + + +class Bgp_neighborsFacts(object): + """ The sonic bgp_neighbors fact class + """ + + neighbor_params_map = { + 'neighbor': 'neighbor-address', + 'peer_as': 'peer-as', + 'peer_type': 'peer-type', + 'peer_group': 'peer-group', + 'keepalive': 'keepalive-interval', + 'holdtime': 'hold-time', + 'connect_retry': 'connect-retry', + 'advertisement_interval': 'minimum-advertisement-interval', + 'bfd_enabled': ['enable-bfd', 'enabled'], + 'check_failure': ['enable-bfd', 'check-control-plane-failure'], + 'profile': ['enable-bfd', 'bfd-profile'], + 'dynamic': 'capability-dynamic', + 'extended_nexthop': 'capability-extended-nexthop', + 'pwd': ['auth-password', 'password'], + 'encrypted': ['auth-password', 'encrypted'], + 'nbr_description': 'description', + 'disable_connected_check': 'disable-ebgp-connected-route-check', + 'dont_negotiate_capability': 'dont-negotiate-capability', + 'enforce_first_as': 'enforce-first-as', + 'enforce_multihop': 'enforce-multihop', + 'local_address': ['transport', 'config', 'local-address'], + 'as': 'local-as', + 'no_prepend': 'local-as-no-prepend', + 'replace_as': 'local-as-replace-as', + 'override_capability': 'override-capability', + 'port': 'peer-port', + 'shutdown_msg': 'shutdown-message', + 'solo': 'solo-peer', + 'strict_capability_match': 'strict-capability-match', + 'ttl_security': 'ttl-security-hops', + 'enabled': ['ebgp-multihop', 'enabled'], + 'multihop_ttl': ['ebgp-multihop', 'multihop-ttl'], + 'v6only': 'openconfig-bgp-ext:v6only', + 'passive': ['transport', 'config', 'passive-mode'] + } + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Bgp_neighborsArgs.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 BGP + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = list() + + if not data: + data = get_all_bgp_neighbors(self._module) + filtered_data = self.filter_neighbors_data(data) + if filtered_data: + data = filtered_data + + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bgp_neighbors', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': remove_empties_from_list(objs)}) + facts['bgp_neighbors'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + + return conf + + def filter_neighbors_data(self, data): + filtered_data = [] + for conf in data: + vrf_name = conf['vrf_name'] + tmp = {} + bgp_as = conf['bgp_as'] + val = None + if 'neighbors' in conf and 'neighbor' in conf['neighbors']: + val = conf['neighbors']['neighbor'] + + tmp['vrf_name'] = vrf_name + tmp['bgp_as'] = bgp_as + peergroup = get_peergroups(self._module, vrf_name) + if peergroup: + tmp['peer_group'] = peergroup + fil_neighbors = [] + if val: + for neighbor in val: + fil_neighbor = get_from_params_map(self.neighbor_params_map, neighbor) + capability = {} + capability_dynamic = fil_neighbor.get('dynamic', None) + if capability_dynamic is not None: + capability['dynamic'] = capability_dynamic + fil_neighbor.pop('dynamic') + capability_extended_nexthop = fil_neighbor.get('extended_nexthop', None) + if capability_extended_nexthop is not None: + capability['extended_nexthop'] = capability_extended_nexthop + fil_neighbor.pop('extended_nexthop') + if capability: + fil_neighbor['capability'] = capability + remote = {} + peer_as = fil_neighbor.get('peer_as', None) + if peer_as is not None: + remote['peer_as'] = peer_as + fil_neighbor.pop('peer_as') + peer_type = fil_neighbor.get('peer_type', None) + if peer_type is not None: + remote['peer_type'] = peer_type.lower() + fil_neighbor.pop('peer_type') + if remote: + fil_neighbor['remote_as'] = remote + auth_pwd = {} + pwd = fil_neighbor.get('pwd', None) + if pwd is not None: + auth_pwd['pwd'] = pwd + fil_neighbor.pop('pwd') + encrypted = fil_neighbor.get('encrypted', None) + if encrypted is not None: + auth_pwd['encrypted'] = encrypted + fil_neighbor.pop('encrypted') + ebgp_multihop = {} + enabled = fil_neighbor.get('enabled', None) + if enabled is not None: + ebgp_multihop['enabled'] = enabled + fil_neighbor.pop('enabled') + multihop_ttl = fil_neighbor.get('multihop_ttl', None) + if multihop_ttl is not None: + ebgp_multihop['multihop_ttl'] = multihop_ttl + fil_neighbor.pop('multihop_ttl') + local_as = {} + asn = fil_neighbor.get('as', None) + if asn is not None: + local_as['as'] = asn + fil_neighbor.pop('as') + no_prepend = fil_neighbor.get('no_prepend', None) + if no_prepend is not None: + local_as['no_prepend'] = no_prepend + fil_neighbor.pop('no_prepend') + replace_as = fil_neighbor.get('replace_as', None) + if replace_as is not None: + local_as['replace_as'] = replace_as + fil_neighbor.pop('replace_as') + bfd = {} + bfd_enabled = fil_neighbor.get('bfd_enabled', None) + if bfd_enabled is not None: + bfd['enabled'] = bfd_enabled + fil_neighbor.pop('bfd_enabled') + check_failure = fil_neighbor.get('check_failure', None) + if check_failure is not None: + bfd['check_failure'] = check_failure + fil_neighbor.pop('check_failure') + profile = fil_neighbor.get('profile', None) + if profile is not None: + bfd['profile'] = profile + fil_neighbor.pop('profile') + if auth_pwd: + fil_neighbor['auth_pwd'] = auth_pwd + if ebgp_multihop: + fil_neighbor['ebgp_multihop'] = ebgp_multihop + if local_as: + fil_neighbor['local_as'] = local_as + if bfd: + fil_neighbor['bfd'] = bfd + if fil_neighbor: + fil_neighbors.append(fil_neighbor) + if fil_neighbors: + tmp['neighbors'] = fil_neighbors + filtered_data.append(tmp) + return filtered_data diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors_af/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors_af/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors_af/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors_af/bgp_neighbors_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors_af/bgp_neighbors_af.py new file mode 100644 index 000000000..26119b61c --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_neighbors_af/bgp_neighbors_af.py @@ -0,0 +1,222 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic bgp_neighbors_af 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 + +import re +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.utils.utils import ( + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.bgp_neighbors_af.bgp_neighbors_af import Bgp_neighbors_afArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( + get_all_bgp_neighbors, + get_from_params_map, + update_bgp_nbr_pg_ip_afi_dict, + update_bgp_nbr_pg_prefix_limit_dict +) + + +class Bgp_neighbors_afFacts(object): + """ The sonic bgp_neighbors_af fact class + """ + + neighbor_af_params_map = { + 'afi': 'afi-safi-name', + 'route_reflector_client': 'route-reflector-client', + 'route_server_client': 'route-server-client', + 'allowas_in_origin': ['allow-own-as', 'origin'], + 'allowas_in_value': ['allow-own-as', 'as-count'], + 'in_route_name': ['apply-policy', 'import-policy'], + 'out_route_name': ['apply-policy', 'export-policy'], + 'activate': 'enabled', + 'prefix_list_in': ['prefix-list', 'import-policy'], + 'prefix_list_out': ['prefix-list', 'export-policy'], + 'ipv4_unicast': 'ipv4-unicast', + 'ipv6_unicast': 'ipv6-unicast', + 'l2vpn_evpn': ['l2vpn-evpn', 'prefix-limit'] + } + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Bgp_neighbors_afArgs.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 fill_route_map(self, data): + for route_map_key in ['out_route_name', 'in_route_name']: + if route_map_key in data: + route_map = data['route_map'] + for e_route in data[route_map_key]: + direction = route_map_key.split('_', maxsplit=1)[0] + route_map.append({'name': e_route, 'direction': direction}) + data.pop(route_map_key) + + def normalize_neighbors_af_data(self, neighbors): + norm_neighbors = [] + + for nei_data in neighbors: + norm_neighbor = {} + + neighbor = nei_data.get('neighbor-address', None) + if not neighbor: + continue + norm_neighbor['neighbor'] = neighbor + norm_neighbor['address_family'] = [] + nei_afs = nei_data.get('afi-safis', None) + if not nei_afs: + if norm_neighbor: + norm_neighbors.append(norm_neighbor) + continue + nei_afs = nei_afs.get('afi-safi', None) + if not nei_afs: + if norm_neighbor: + norm_neighbors.append(norm_neighbor) + continue + norm_neighbor_afs = [] + for nei_af in nei_afs: + norm_nei_af = get_from_params_map(self.neighbor_af_params_map, nei_af) + if norm_nei_af: + if 'activate' not in norm_nei_af: + norm_nei_af['activate'] = False + if 'route_server_client' not in norm_nei_af: + norm_nei_af['route_server_client'] = False + norm_nei_af['route_map'] = [] + self.fill_route_map(norm_nei_af) + + allowas_in = {} + allowas_in_origin = norm_nei_af.get('allowas_in_origin', None) + if allowas_in_origin is not None: + allowas_in['origin'] = allowas_in_origin + norm_nei_af.pop('allowas_in_origin') + + allowas_in_value = norm_nei_af.get('allowas_in_value', None) + if allowas_in_value is not None: + allowas_in['value'] = allowas_in_value + norm_nei_af.pop('allowas_in_value') + if allowas_in: + norm_nei_af['allowas_in'] = allowas_in + + ipv4_unicast = norm_nei_af.get('ipv4_unicast', None) + ipv6_unicast = norm_nei_af.get('ipv6_unicast', None) + l2vpn_evpn = norm_nei_af.get('l2vpn_evpn', None) + if ipv4_unicast: + if 'config' in ipv4_unicast: + ip_afi = update_bgp_nbr_pg_ip_afi_dict(ipv4_unicast['config']) + if ip_afi: + norm_nei_af['ip_afi'] = ip_afi + if 'prefix-limit' in ipv4_unicast and 'config' in ipv4_unicast['prefix-limit']: + prefix_limit = update_bgp_nbr_pg_prefix_limit_dict(ipv4_unicast['prefix-limit']['config']) + if prefix_limit: + norm_nei_af['prefix_limit'] = prefix_limit + norm_nei_af.pop('ipv4_unicast') + elif ipv6_unicast: + if 'config' in ipv6_unicast: + ip_afi = update_bgp_nbr_pg_ip_afi_dict(ipv6_unicast['config']) + if ip_afi: + norm_nei_af['ip_afi'] = ip_afi + if 'prefix-limit' in ipv6_unicast and 'config' in ipv6_unicast['prefix-limit']: + prefix_limit = update_bgp_nbr_pg_prefix_limit_dict(ipv6_unicast['prefix-limit']['config']) + if prefix_limit: + norm_nei_af['prefix_limit'] = prefix_limit + norm_nei_af.pop('ipv6_unicast') + elif l2vpn_evpn: + if 'config' in l2vpn_evpn: + prefix_limit = update_bgp_nbr_pg_prefix_limit_dict(l2vpn_evpn['config']) + if prefix_limit: + norm_nei_af['prefix_limit'] = prefix_limit + norm_nei_af.pop('l2vpn_evpn') + + norm_neighbor_afs.append(norm_nei_af) + if norm_neighbor_afs: + norm_neighbor['address_family'] = norm_neighbor_afs + if norm_neighbor: + norm_neighbors.append(norm_neighbor) + return norm_neighbors + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for BGP + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = list() + if connection: # just for linting purposes, remove + pass + + if not data: + data = get_all_bgp_neighbors(self._module) + + new_data = [] + for conf in data: + if not conf: + continue + new_item = {} + new_item['bgp_as'] = conf['bgp_as'] + new_item['vrf_name'] = conf['vrf_name'] + neighbors = conf.get('neighbors', None) + if not neighbors: + new_data.append(new_item) + continue + neighbors = neighbors.get('neighbor', None) + if not neighbors: + new_data.append(new_item) + continue + + new_neighbors = self.normalize_neighbors_af_data(neighbors) + if new_neighbors: + new_item['neighbors'] = new_neighbors + if new_item: + new_data.append(new_item) + + # operate on a collection of resource x + for conf in new_data: + if conf: + obj = self.render_config(self.generated_spec, conf) + # split the config into instances of the resource + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bgp_neighbors_af', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': remove_empties_from_list(objs)}) + facts['bgp_neighbors_af'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/facts.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/facts.py new file mode 100644 index 000000000..75622632a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/facts.py @@ -0,0 +1,101 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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 facts class for sonic +this file validates each subset of facts and selectively +calls the appropriate facts gathering function +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.facts.facts import FactsArgs +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts import ( + FactsBase, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.vlans.vlans import VlansFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.interfaces.interfaces import InterfacesFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.l2_interfaces.l2_interfaces import L2_interfacesFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.l3_interfaces.l3_interfaces import L3_interfacesFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bgp.bgp import BgpFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bgp_af.bgp_af import Bgp_afFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bgp_neighbors.bgp_neighbors import Bgp_neighborsFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bgp_neighbors_af.bgp_neighbors_af import Bgp_neighbors_afFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bgp_as_paths.bgp_as_paths import Bgp_as_pathsFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bgp_communities.bgp_communities import Bgp_communitiesFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bgp_ext_communities.bgp_ext_communities import ( + Bgp_ext_communitiesFacts, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.mclag.mclag import MclagFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.prefix_lists.prefix_lists import Prefix_listsFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.vrfs.vrfs import VrfsFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.vxlans.vxlans import VxlansFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.users.users import UsersFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.port_breakout.port_breakout import Port_breakoutFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.aaa.aaa import AaaFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.tacacs_server.tacacs_server import Tacacs_serverFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.system.system import SystemFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.radius_server.radius_server import Radius_serverFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.static_routes.static_routes import Static_routesFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.ntp.ntp import NtpFacts + +FACT_LEGACY_SUBSETS = {} +FACT_RESOURCE_SUBSETS = dict( + vlans=VlansFacts, + interfaces=InterfacesFacts, + l2_interfaces=L2_interfacesFacts, + l3_interfaces=L3_interfacesFacts, + lag_interfaces=Lag_interfacesFacts, + bgp=BgpFacts, + bgp_af=Bgp_afFacts, + bgp_neighbors=Bgp_neighborsFacts, + bgp_neighbors_af=Bgp_neighbors_afFacts, + bgp_as_paths=Bgp_as_pathsFacts, + bgp_communities=Bgp_communitiesFacts, + bgp_ext_communities=Bgp_ext_communitiesFacts, + mclag=MclagFacts, + prefix_lists=Prefix_listsFacts, + vrfs=VrfsFacts, + vxlans=VxlansFacts, + users=UsersFacts, + system=SystemFacts, + port_breakout=Port_breakoutFacts, + aaa=AaaFacts, + tacacs_server=Tacacs_serverFacts, + radius_server=Radius_serverFacts, + static_routes=Static_routesFacts, + ntp=NtpFacts, +) + + +class Facts(FactsBase): + """ The fact class for sonic + """ + + VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) + VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) + + def __init__(self, module): + super(Facts, self).__init__(module) + + def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None): + """ Collect the facts for sonic + + :param legacy_facts_type: List of legacy facts types + :param resource_facts_type: List of resource fact types + :param data: previously collected conf + :rtype: dict + :return: the facts gathered + """ + netres_choices = FactsArgs.argument_spec['gather_network_resources'].get('choices', []) + if self.VALID_RESOURCE_SUBSETS: + self.get_network_resources_facts(FACT_RESOURCE_SUBSETS, resource_facts_type, data) + + if self.VALID_LEGACY_GATHER_SUBSETS: + self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type) + + return self.ansible_facts, self._warnings diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/interfaces/interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/interfaces/interfaces.py new file mode 100644 index 000000000..a36b5d3c0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/interfaces/interfaces.py @@ -0,0 +1,147 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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 interfaces 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 + +import re +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.interfaces.interfaces import InterfacesArgs +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 InterfacesFacts(object): + """ The sonic interfaces fact class + """ + loop_backs = "," + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = InterfacesArgs.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 get_all_interfaces(self): + """Get all the interfaces available in chassis""" + all_interfaces = {} + request = [{"path": "data/openconfig-interfaces:interfaces", "method": GET}] + 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-interfaces:interfaces" in response[0][1]: + all_interfaces = response[0][1].get("openconfig-interfaces:interfaces", {}) + return all_interfaces['interface'] + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + if connection: # just for linting purposes, remove + pass + + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_interfaces() + # operate on a collection of resource x + self.reset_loop_backs() + + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + obj = self.transform_config(obj) + # split the config into instances of the resource + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('interfaces', None) + facts = {} + if objs: + facts['interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + if params: + facts['interfaces'].extend(params['config']) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def transform_config(self, conf): + + exist_cfg = conf['config'] + trans_cfg = None + + is_loop_back = False + name = conf['name'] + if name.startswith('Loopback'): + is_loop_back = True + pos = name.find('|') + if pos > 0: + name = name[0:pos] + + if not (is_loop_back and self.is_loop_back_already_esist(name)) and (name != "eth0"): + trans_cfg = dict() + trans_cfg['name'] = name + if is_loop_back: + self.update_loop_backs(name) + else: + trans_cfg['enabled'] = exist_cfg['enabled'] if exist_cfg.get('enabled') is not None else True + trans_cfg['description'] = exist_cfg['description'] if exist_cfg.get('description') else "" + trans_cfg['mtu'] = exist_cfg['mtu'] if exist_cfg.get('mtu') else 9100 + + return trans_cfg + + def reset_loop_backs(self): + self.loop_backs = "," + + def update_loop_backs(self, loop_back): + self.loop_backs += "{0},".format(loop_back) + + def is_loop_back_already_esist(self, loop_back): + return (",{0},".format(loop_back) in self.loop_backs) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_interfaces/l2_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_interfaces/l2_interfaces.py new file mode 100644 index 000000000..07d7f97dd --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_interfaces/l2_interfaces.py @@ -0,0 +1,160 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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 l2_interfaces 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 + +import re +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.l2_interfaces.l2_interfaces import L2_interfacesArgs +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 L2_interfacesFacts(object): + """ The sonic l2_interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = L2_interfacesArgs.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 vlan_range_to_list(self, in_range): + range_bounds = in_range.split('-') + range_bottom = int(range_bounds[0]) + range_top = int(range_bounds[1]) + 1 + vlan_list = list(range(range_bottom, range_top)) + vlan_dict_list = [] + for vlan in vlan_list: + vlan_dict_list.append({'vlan': vlan}) + return vlan_dict_list + + def get_l2_interfaces_from_interfaces(self, interfaces): + l2_interfaces = [] + + for intf in interfaces: + name = intf['name'] + key = 'openconfig-if-ethernet:ethernet' + if name.startswith('PortChannel'): + key = 'openconfig-if-aggregate:aggregation' + eth_det = intf.get(key) + if eth_det: + open_cfg_vlan = eth_det.get('openconfig-vlan:switched-vlan') + if open_cfg_vlan: + new_det = dict() + new_det['name'] = name + if name == "eth0": + continue + if (open_cfg_vlan['config'].get('access-vlan')): + new_det['access'] = dict({'vlan': open_cfg_vlan['config'].get('access-vlan')}) + if (open_cfg_vlan['config'].get('trunk-vlans')): + new_det['trunk'] = {} + new_det['trunk']['allowed_vlans'] = [] + + # Save trunk vlans as a list of single vlan dicts: Convert + # any ranges to lists of individual vlan dicts and merge + # each resulting "range list" onto the main list for the + # interface. + for vlan in open_cfg_vlan['config'].get('trunk-vlans'): + if isinstance(vlan, str) and '-' in vlan: + new_det['trunk']['allowed_vlans'].extend(self.vlan_range_to_list(vlan)) + else: + new_det['trunk']['allowed_vlans'].append({'vlan': vlan}) + l2_interfaces.append(new_det) + + return l2_interfaces + + def get_all_l2_interfaces(self): + """Get all the l2_interfaces available in chassis""" + l2_interfaces = {} + request = [{"path": "data/openconfig-interfaces:interfaces", "method": GET}] + 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-interfaces:interfaces" in response[0][1]: + interfaces = response[0][1].get("openconfig-interfaces:interfaces", {}) + if interfaces.get("interface"): + interfaces = interfaces['interface'] + l2_interfaces = self.get_l2_interfaces_from_interfaces(interfaces) + else: + l2_interfaces = {} + + return l2_interfaces + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for l2_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_l2_interfaces() + + objs = list() + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + # split the config into instances of the resource + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('l2_interfaces', None) + facts = {} + if objs: + facts['l2_interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['l2_interfaces'].append(utils.remove_empties(cfg)) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_interfaces/l3_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_interfaces/l3_interfaces.py new file mode 100644 index 000000000..69a6dcd44 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_interfaces/l3_interfaces.py @@ -0,0 +1,185 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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 l3_interfaces 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 + +import re +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.l3_interfaces.l3_interfaces import L3_interfacesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + + +class L3_interfacesFacts(object): + """ The sonic l3_interfaces fact class + """ + + loop_backs = "," + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = L3_interfacesArgs.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 get_l3_interfaces(self): + url = "data/openconfig-interfaces:interfaces/interface" + method = "GET" + request = [{"path": url, "method": method}] + + 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) + + l3_lists = [] + if "openconfig-interfaces:interface" in response[0][1]: + l3_lists = response[0][1].get("openconfig-interfaces:interface", []) + + l3_configs = [] + for l3 in l3_lists: + l3_dict = dict() + l3_name = l3["name"] + if l3_name == "eth0": + continue + + l3_dict['name'] = l3_name + + ip = None + anycast_addr = list() + if l3.get('openconfig-vlan:routed-vlan'): + ip = l3['openconfig-vlan:routed-vlan'] + if ip.get('openconfig-if-ip:ipv4', None) and ip['openconfig-if-ip:ipv4'].get('openconfig-interfaces-ext:sag-ipv4', None): + if ip['openconfig-if-ip:ipv4']['openconfig-interfaces-ext:sag-ipv4'].get('config', None): + if ip['openconfig-if-ip:ipv4']['openconfig-interfaces-ext:sag-ipv4']['config'].get('static-anycast-gateway', None): + anycast_addr = ip['openconfig-if-ip:ipv4']['openconfig-interfaces-ext:sag-ipv4']['config']['static-anycast-gateway'] + else: + ip = l3.get('subinterfaces', {}).get('subinterface', [{}])[0] + + l3_dict['ipv4'] = dict() + l3_ipv4 = list() + if anycast_addr: + l3_dict['ipv4']['anycast_addresses'] = anycast_addr + elif 'openconfig-if-ip:ipv4' in ip and 'addresses' in ip['openconfig-if-ip:ipv4'] and 'address' in ip['openconfig-if-ip:ipv4']['addresses']: + for ipv4 in ip['openconfig-if-ip:ipv4']['addresses']['address']: + if ipv4.get('config') and ipv4.get('config').get('ip'): + temp = dict() + temp['address'] = str(ipv4['config']['ip']) + '/' + str(ipv4['config']['prefix-length']) + temp['secondary'] = ipv4['config']['secondary'] + l3_ipv4.append(temp) + if l3_ipv4: + l3_dict['ipv4']['addresses'] = l3_ipv4 + + l3_dict['ipv6'] = dict() + l3_ipv6 = list() + if 'openconfig-if-ip:ipv6' in ip: + if 'addresses' in ip['openconfig-if-ip:ipv6'] and 'address' in ip['openconfig-if-ip:ipv6']['addresses']: + for ipv6 in ip['openconfig-if-ip:ipv6']['addresses']['address']: + if ipv6.get('config') and ipv6.get('config').get('ip'): + temp = dict() + temp['address'] = str(ipv6['config']['ip']) + '/' + str(ipv6['config']['prefix-length']) + l3_ipv6.append(temp) + if l3_ipv6: + l3_dict['ipv6']['addresses'] = l3_ipv6 + if 'config' in ip['openconfig-if-ip:ipv6'] and 'enabled' in ip['openconfig-if-ip:ipv6']['config']: + l3_dict['ipv6']['enabled'] = ip['openconfig-if-ip:ipv6']['config']['enabled'] + + l3_configs.append(l3_dict) + return l3_configs + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for l3_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + if not data: + resources = self.get_l3_interfaces() + objs = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + obj = self.transform_config(obj) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('l3_interfaces', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['l3_interfaces'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def transform_config(self, conf): + exist_cfg = conf + trans_cfg = None + + is_loop_back = False + name = exist_cfg['name'] + if name.startswith('Loopback'): + is_loop_back = True + pos = name.find('|') + if pos > 0: + name = name[0:pos] + + if not (is_loop_back and self.is_loop_back_already_esist(name)) and (name != "eth0"): + trans_cfg = dict() + trans_cfg['name'] = name + if is_loop_back: + self.update_loop_backs(name) + trans_cfg['ipv4'] = exist_cfg.get('ipv4', {}) + trans_cfg['ipv6'] = exist_cfg.get('ipv6', {}) + + return trans_cfg + + def reset_loop_backs(self): + self.loop_backs = "," + + def update_loop_backs(self, loop_back): + self.loop_backs += "{Loopback},".format(Loopback=loop_back) + + def is_loop_back_already_esist(self, loop_back): + return (",{0},".format(loop_back) in self.loop_backs) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lag_interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lag_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lag_interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lag_interfaces/lag_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lag_interfaces/lag_interfaces.py new file mode 100644 index 000000000..728196813 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lag_interfaces/lag_interfaces.py @@ -0,0 +1,135 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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 lag_interfaces 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 + +import re +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.lag_interfaces.lag_interfaces import Lag_interfacesArgs +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 Lag_interfacesFacts(object): + """ The sonic lag_interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Lag_interfacesArgs.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 get_all_portchannels(self): + """Get all the interfaces available in chassis""" + request = [{"path": "data/sonic-portchannel:sonic-portchannel", "method": GET}] + 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 response[0][1]: + data = response[0][1]['sonic-portchannel:sonic-portchannel'] + else: + data = [] + if data is not None: + if "PORTCHANNEL_MEMBER" in data: + portchannel_members_list = data["PORTCHANNEL_MEMBER"]["PORTCHANNEL_MEMBER_LIST"] + else: + portchannel_members_list = [] + if "PORTCHANNEL" in data: + portchannel_list = data["PORTCHANNEL"]["PORTCHANNEL_LIST"] + else: + portchannel_list = [] + if portchannel_list: + for i in portchannel_list: + if not any(d["name"] == i["name"] for d in portchannel_members_list): + portchannel_members_list.append({'ifname': None, 'name': i['name']}) + if data: + return portchannel_members_list + else: + return [] + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for lag_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + if not data: + data = self.get_all_portchannels() + # operate on a collection of resource x + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + obj = self.transform_config(obj) + if obj: + self.merge_portchannels(objs, obj) + facts = {} + if objs: + facts['lag_interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['lag_interfaces'].append(cfg) + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + return conf + + def transform_config(self, conf): + trans_cfg = dict() + trans_cfg['name'] = conf['name'] + trans_cfg['members'] = dict() + if conf['ifname']: + interfaces = list() + interface = {'member': conf['ifname']} + interfaces.append(interface) + trans_cfg['members'] = {'interfaces': interfaces} + return trans_cfg + + def merge_portchannels(self, configs, conf): + if len(configs) == 0: + configs.append(conf) + else: + new_interface = None + if conf.get('members') and conf['members'].get('interfaces'): + new_interface = conf['members']['interfaces'][0] + else: + configs.append(conf) + if new_interface: + matched = next((cfg for cfg in configs if cfg['name'] == conf['name']), None) + if matched and matched.get('members'): + ext_interfaces = matched.get('members').get('interfaces', []) + ext_interfaces.append(new_interface) + else: + configs.append(conf) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mclag/mclag.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mclag/mclag.py new file mode 100644 index 000000000..69864cdf9 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mclag/mclag.py @@ -0,0 +1,139 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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 mclag 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 + +import re +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.mclag.mclag import MclagArgs +from ansible.module_utils.connection import ConnectionError + +GET = "get" + + +class MclagFacts(object): + """ The sonic mclag fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = MclagArgs.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 get_all_mclag(self): + """Get all the mclag available in chassis""" + request = [{"path": "data/openconfig-mclag:mclag", "method": GET}] + 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-mclag:mclag' in response[0][1]): + data = response[0][1]['openconfig-mclag:mclag'] + else: + data = {} + return data + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for mclag + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = None + if not data: + data = self.get_all_mclag() + if data: + objs = self.render_config(self.generated_spec, data) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['mclag'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = self.parse_sonic_mclag(spec, conf) + return config + + def parse_sonic_mclag(self, spec, conf): + config = {} + portchannels_list = [] + if conf: + domain_data = None + if conf.get('mclag-domains', None) and conf['mclag-domains'].get('mclag-domain', None): + domain_data = conf['mclag-domains']['mclag-domain'][0] + if domain_data: + domain_id = domain_data['domain-id'] + config['domain_id'] = domain_id + domain_config = domain_data.get('config', None) + if domain_config: + if domain_config.get('session-timeout', None): + config['session_timeout'] = domain_config['session-timeout'] + if domain_config.get('keepalive-interval', None): + config['keepalive'] = domain_config['keepalive-interval'] + if domain_config.get('source-address', None): + config['source_address'] = domain_config['source-address'] + if domain_config.get('peer-address', None): + config['peer_address'] = domain_config['peer-address'] + if domain_config.get('peer-link', None): + config['peer_link'] = domain_config['peer-link'] + if domain_config.get('mclag-system-mac', None): + config['system_mac'] = domain_config['mclag-system-mac'] + + if conf.get('vlan-interfaces', None) and conf['vlan-interfaces'].get('vlan-interface', None): + vlans_list = [] + vlan_data = conf['vlan-interfaces']['vlan-interface'] + for vlan in vlan_data: + vlans_list.append({'vlan': vlan['name']}) + if vlans_list: + config['unique_ip'] = {'vlans': vlans_list} + + if conf.get('interfaces', None) and conf['interfaces'].get('interface', None): + portchannels_list = [] + po_data = conf['interfaces']['interface'] + for po in po_data: + if po.get('config', None) and po['config'].get('mclag-domain-id', None) and domain_id == domain_data['domain-id']: + portchannels_list.append({'lag': po['name']}) + if portchannels_list: + config['members'] = {'portchannels': portchannels_list} + + return config diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ntp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ntp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ntp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ntp/ntp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ntp/ntp.py new file mode 100644 index 000000000..a47142b47 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ntp/ntp.py @@ -0,0 +1,153 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 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 ntp 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 + +import re +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.ntp.ntp import NtpArgs +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 NtpFacts(object): + """ The sonic ntp fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = NtpArgs.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 ntp + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_ntp_configuration() + + obj = self.render_config(self.generated_spec, data) + + ansible_facts['ansible_network_resources'].pop('ntp', None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {'config': obj}) + facts['ntp'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def get_ntp_configuration(self): + """Get all NTP configuration""" + + all_ntp_request = [{"path": "data/openconfig-system:system/ntp", "method": GET}] + all_ntp_response = [] + try: + all_ntp_response = edit_config(self._module, to_request(self._module, all_ntp_request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + all_ntp_config = dict() + if 'openconfig-system:ntp' in all_ntp_response[0][1]: + all_ntp_config = all_ntp_response[0][1].get('openconfig-system:ntp', {}) + + ntp_global_config = dict() + if 'config' in all_ntp_config: + ntp_global_config = all_ntp_config.get('config', {}) + + ntp_servers = [] + if 'servers' in all_ntp_config: + ntp_servers = all_ntp_config['servers'].get('server', []) + + ntp_keys = [] + if 'ntp-keys' in all_ntp_config: + ntp_keys = all_ntp_config['ntp-keys'].get('ntp-key', []) + + ntp_config = dict() + + if 'network-instance' in ntp_global_config: + ntp_config['vrf'] = ntp_global_config['network-instance'] + + if 'enable-ntp-auth' in ntp_global_config: + ntp_config['enable_ntp_auth'] = ntp_global_config['enable-ntp-auth'] + + if 'source-interface' in ntp_global_config: + ntp_config['source_interfaces'] = ntp_global_config['source-interface'] + + if 'trusted-key' in ntp_global_config: + ntp_config['trusted_keys'] = ntp_global_config['trusted-key'] + + servers = [] + for ntp_server in ntp_servers: + if 'config' in ntp_server: + server = {} + server['address'] = ntp_server['config'].get('address', None) + if 'key-id' in ntp_server['config']: + server['key_id'] = ntp_server['config']['key-id'] + server['minpoll'] = ntp_server['config'].get('minpoll', None) + server['maxpoll'] = ntp_server['config'].get('maxpoll', None) + servers.append(server) + ntp_config['servers'] = servers + + keys = [] + for ntp_key in ntp_keys: + if 'config' in ntp_key: + key = {} + key['encrypted'] = ntp_key['config'].get('encrypted', None) + key['key_id'] = ntp_key['config'].get('key-id', None) + key_type_str = ntp_key['config'].get('key-type', None) + key_type = key_type_str.split(":", 1)[-1] + key['key_type'] = key_type + key['key_value'] = ntp_key['config'].get('key-value', None) + keys.append(key) + ntp_config['ntp_keys'] = keys + + return ntp_config diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/port_breakout/port_breakout.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/port_breakout/port_breakout.py new file mode 100644 index 000000000..938bd6423 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/port_breakout/port_breakout.py @@ -0,0 +1,125 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic port breakout 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 +import re +import json +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.port_breakout.port_breakout import Port_breakoutArgs +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.utils import ( + get_breakout_mode, +) +from ansible.module_utils.connection import ConnectionError + +GET = "get" +POST = "post" + + +class Port_breakoutFacts(object): + """ The sonic port breakout fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Port_breakoutArgs.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 port_breakout + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_port_breakout() + + objs = list() + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('port_breakout', None) + facts = {} + if objs: + facts['port_breakout'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + if params: + facts['port_breakout'].extend(params['config']) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def get_all_port_breakout(self): + """Get all the port_breakout configured in the device""" + request = [{"path": "operations/sonic-port-breakout:breakout_capabilities", "method": POST}] + port_breakout_list = [] + 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) + + raw_port_breakout_list = [] + if "sonic-port-breakout:output" in response[0][1]: + raw_port_breakout_list = response[0][1].get("sonic-port-breakout:output", {}).get('caps', []) + + for port_breakout in raw_port_breakout_list: + name = port_breakout.get('port', None) + mode = port_breakout.get('defmode', None) + if name and mode: + if '[' in mode: + mode = mode[:mode.index('[')] + norm_port_breakout = {'name': name, 'mode': mode} + mode = get_breakout_mode(self._module, name) + if mode: + norm_port_breakout['mode'] = mode + port_breakout_list.append(norm_port_breakout) + + return port_breakout_list diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/prefix_lists/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/prefix_lists/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/prefix_lists/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/prefix_lists/prefix_lists.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/prefix_lists/prefix_lists.py new file mode 100644 index 000000000..e246b5720 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/prefix_lists/prefix_lists.py @@ -0,0 +1,158 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The sonic prefix_lists 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.utils.utils \ + import ( + remove_empties_from_list + ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.prefix_lists.prefix_lists import Prefix_listsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + + +def prefix_set_cfg_parse(unparsed_prefix_set): + '''Parse the raw input configuration JSON representation for the prefix set specified + by the input "unparsed_prefix_set" input parameter. Parse the information to + convert it to a dictionary matching the "argspec" for the "prefix_lists" resource + module.''' + + parsed_prefix_set = dict() + if not unparsed_prefix_set.get("config"): + return parsed_prefix_set + parsed_prefix_set['name'] = unparsed_prefix_set['name'] + pfx_cfg = unparsed_prefix_set['config'] + if pfx_cfg.get('mode') and isinstance((pfx_cfg['mode']), str): + parsed_prefix_set['afi'] = pfx_cfg['mode'].lower() + if unparsed_prefix_set.get('openconfig-routing-policy-ext:extended-prefixes'): + prefix_lists_container = \ + unparsed_prefix_set['openconfig-routing-policy-ext:extended-prefixes'] + if not prefix_lists_container.get("extended-prefix"): + return parsed_prefix_set + prefix_lists_unparsed = prefix_lists_container['extended-prefix'] + + prefix_lists_parsed = [] + for prefix_entry_unparsed in prefix_lists_unparsed: + if not prefix_entry_unparsed.get('config'): + continue + if not prefix_entry_unparsed['config'].get('action'): + continue + prefix_entry_cfg = prefix_entry_unparsed['config'] + prefix_parsed = dict() + prefix_parsed['action'] = prefix_entry_cfg['action'].lower() + if not prefix_entry_unparsed.get('ip-prefix'): + continue + if not prefix_entry_unparsed.get('sequence-number'): + continue + + prefix_parsed['prefix'] = prefix_entry_unparsed['ip-prefix'] + prefix_parsed['sequence'] = prefix_entry_unparsed['sequence-number'] + if (prefix_entry_unparsed.get('masklength-range') and + (not prefix_entry_unparsed['masklength-range'] == 'exact')): + mask = int(prefix_parsed['prefix'].split('/')[1]) + ge_le = prefix_entry_unparsed['masklength-range'].split('..') + ge_bound = int(ge_le[0]) + if ge_bound != mask: + prefix_parsed['ge'] = ge_bound + pfx_len = 32 if parsed_prefix_set['afi'] == 'ipv4' else 128 + le_bound = int(ge_le[1]) + if le_bound != pfx_len: + prefix_parsed['le'] = le_bound + prefix_lists_parsed.append(prefix_parsed) + parsed_prefix_set['prefixes'] = prefix_lists_parsed + return parsed_prefix_set + + +class Prefix_listsFacts: + """ The sonic prefix_lists fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Prefix_listsArgs.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 get_all_prefix_sets(self): + '''Execute a REST "GET" API to fetch all of the current prefix list configuration + from the target device.''' + + pfx_fetch_spec = "openconfig-routing-policy:routing-policy/defined-sets/prefix-sets" + pfx_resp_key = "openconfig-routing-policy:prefix-sets" + pfx_set_key = "prefix-set" + # pfx_short_spec = "openconfig-routing-policy:prefix-set" + url = "data/%s" % pfx_fetch_spec + method = "GET" + request = [{"path": url, "method": method}] + + try: + response = edit_config(self._module, to_request(self._module, request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc)) + + prefix_lists_unparsed = [] + resp_prefix_set = response[0][1].get(pfx_resp_key, None) + if resp_prefix_set: + prefix_lists_unparsed = resp_prefix_set.get(pfx_set_key, None) + return prefix_lists_unparsed + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for prefix_lists + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # (comment by Ansible): just for linting purposes, remove + pass + + if not data: + # Fetch data from the current device configuration + # (Skip if operating on previously fetched configuration.) + data = self.get_all_prefix_sets() + + # split the unparsed prefix configuration list into a list + # of parsed prefix set "instances" (dictonary "objects"). + prefix_sets = list() + for prefix_set_cfg in data: + prefix_set = prefix_set_cfg_parse(prefix_set_cfg) + if prefix_set: + prefix_sets.append(prefix_set) + + ansible_facts['ansible_network_resources'].pop('prefix_lists', None) + facts = {} + if prefix_sets: + params = utils.validate_config(self.argument_spec, + {'config': remove_empties_from_list(prefix_sets)}) + facts['prefix_lists'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/radius_server/radius_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/radius_server/radius_server.py new file mode 100644 index 000000000..72593b225 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/radius_server/radius_server.py @@ -0,0 +1,168 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic tacas server 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 +import re +import json +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.radius_server.radius_server import Radius_serverArgs +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 Radius_serverFacts(object): + """ The sonic tacas server fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Radius_serverArgs.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 radius_server + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + obj = None + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_radius_server() + + obj = self.render_config(self.generated_spec, data) + + ansible_facts['ansible_network_resources'].pop('radius_server', None) + facts = {} + if obj: + facts['radius_server'] = {} + params = utils.validate_config(self.argument_spec, {'config': obj}) + if params: + facts['radius_server'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def get_all_radius_server(self): + """Get all the radius_server configured in the device""" + request = [{"path": "data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/config", "method": GET}] + radius_server_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:config" in response[0][1]: + raw_radius_global_data = response[0][1].get("openconfig-system:config", {}) + + if 'auth-type' in raw_radius_global_data: + radius_server_data['auth_type'] = raw_radius_global_data['auth-type'] + if 'secret-key' in raw_radius_global_data: + radius_server_data['key'] = raw_radius_global_data['secret-key'] + if 'timeout' in raw_radius_global_data: + radius_server_data['timeout'] = raw_radius_global_data['timeout'] + + request = [{"path": "data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/openconfig-aaa-radius-ext:radius/config", "method": GET}] + 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-aaa-radius-ext:config" in response[0][1]: + raw_radius_ext_global_data = response[0][1].get("openconfig-aaa-radius-ext:config", {}) + + if 'nas-ip-address' in raw_radius_ext_global_data: + radius_server_data['nas_ip'] = raw_radius_ext_global_data['nas-ip-address'] + if 'retransmit-attempts' in raw_radius_ext_global_data: + radius_server_data['retransmit'] = raw_radius_ext_global_data['retransmit-attempts'] + if 'statistics' in raw_radius_ext_global_data: + radius_server_data['statistics'] = raw_radius_ext_global_data['statistics'] + + request = [{"path": "data/openconfig-system:system/aaa/server-groups/server-group=RADIUS/servers", "method": GET}] + hosts = [] + 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) + + raw_radius_server_list = [] + if "openconfig-system:servers" in response[0][1]: + raw_radius_server_list = response[0][1].get("openconfig-system:servers", {}).get('server', []) + + for radius_host in raw_radius_server_list: + host_data = {} + if 'address' in radius_host: + host_data['name'] = radius_host['address'] + cfg = radius_host.get('config', None) + if cfg: + if 'auth-type' in cfg: + host_data['auth_type'] = cfg['auth-type'] + if 'priority' in cfg: + host_data['priority'] = cfg['priority'] + if 'vrf' in cfg: + host_data['vrf'] = cfg['vrf'] + if 'timeout' in cfg: + host_data['timeout'] = cfg['timeout'] + if radius_host.get('radius', None) and radius_host['radius'].get('config', None): + tacas_cfg = radius_host['radius']['config'] + if tacas_cfg.get('auth-port', None): + host_data['port'] = tacas_cfg['auth-port'] + if tacas_cfg.get('secret-key', None): + host_data['key'] = tacas_cfg['secret-key'] + if tacas_cfg.get('openconfig-aaa-radius-ext:source-interface', None): + host_data['source_interface'] = tacas_cfg['openconfig-aaa-radius-ext:source-interface'] + if tacas_cfg.get('retransmit-attempts', None): + host_data['retransmit'] = tacas_cfg['retransmit-attempts'] + if host_data: + hosts.append(host_data) + + if hosts: + radius_server_data['servers'] = {'host': hosts} + + return radius_server_data diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/static_routes/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/static_routes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/static_routes/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/static_routes/static_routes.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/static_routes/static_routes.py new file mode 100644 index 000000000..f83566440 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/static_routes/static_routes.py @@ -0,0 +1,173 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 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 static_routes 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 + +import re +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.utils.utils import ( + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.static_routes.static_routes import Static_routesArgs +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.bgp_utils import ( + get_all_vrfs, +) + +network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' +protocol_static_routes_path = 'protocols/protocol=STATIC,static/static-routes' + + +class Static_routesFacts(object): + """ The sonic static_routes fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Static_routesArgs.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 static_routes + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + if connection: # just for linting purposes, remove + pass + + if not data: + static_routes_config = self.get_static_routes(self._module) + data = self.update_static_routes(static_routes_config) + # operate on a collection of resource x + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + # split the config into instances of the resource + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('static_routes', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': remove_empties_from_list(objs)}) + facts['static_routes'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + + return conf + + def get_static_routes(self, module): + all_static_routes = [] + vrfs = get_all_vrfs(module) + for vrf_name in vrfs: + get_path = '%s=%s/%s' % (network_instance_path, vrf_name, protocol_static_routes_path) + request = {'path': get_path, 'method': 'get'} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + for resp in response: + if 'openconfig-network-instance:static-routes' in resp[1]: + static_routes_dict = resp[1].get('openconfig-network-instance:static-routes', {}) + static_routes_dict['vrf'] = vrf_name + all_static_routes.append(static_routes_dict) + return all_static_routes + + def update_static_routes(self, data): + static_vrf_list = [] + for static_route in data: + static_vrf_dict = {} + static_route_list = static_route.get('static', []) + vrf_name = static_route.get('vrf', None) + static_list = [] + for static in static_route_list: + static_dict = {} + prefix = static.get('prefix', None) + next_hops = static.get('next-hops', None) + next_hop_list = next_hops.get('next-hop', []) + next_hop_dict_list = [] + for next_hop in next_hop_list: + next_hop_dict = {} + index_dict = {} + inf_ref = next_hop.get('interface-ref', {}) + inf_ref_cfg = inf_ref.get('config', {}) + interface = inf_ref_cfg.get('interface', None) + config = next_hop.get('config', {}) + next_hop_attr = config.get('next-hop', None) + metric = config.get('metric', None) + nexthop_vrf = config.get('network-instance', None) + blackhole = config.get('blackhole', None) + track = config.get('track', None) + tag = config.get('tag', None) + if blackhole: + index_dict['blackhole'] = blackhole + if interface: + index_dict['interface'] = interface + if nexthop_vrf: + index_dict['nexthop_vrf'] = nexthop_vrf + if next_hop_attr: + index_dict['next_hop'] = next_hop_attr + if index_dict: + next_hop_dict['index'] = index_dict + if metric: + next_hop_dict['metric'] = metric + if track: + next_hop_dict['track'] = track + if tag: + next_hop_dict['tag'] = tag + if next_hop_dict: + next_hop_dict_list.append(next_hop_dict) + if prefix: + static_dict['prefix'] = prefix + if next_hop_dict_list: + static_dict['next_hops'] = next_hop_dict_list + if static_dict: + static_list.append(static_dict) + if static_list: + static_vrf_dict['static_list'] = static_list + if vrf_name: + static_vrf_dict['vrf_name'] = vrf_name + if static_vrf_dict: + static_vrf_list.append(static_vrf_dict) + + return static_vrf_list diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/system/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/system/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/system/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/system/system.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/system/system.py new file mode 100644 index 000000000..1d7a82d83 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/system/system.py @@ -0,0 +1,143 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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 system 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 +import re +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.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.system.system import SystemArgs + +GET = "get" + + +class SystemFacts(object): + """ The sonic system fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = SystemArgs.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 get_system(self): + """Get system hostname available in chassis""" + request = [{"path": "data/openconfig-system:system/config", "method": GET}] + 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:config' in response[0][1]): + data = response[0][1]['openconfig-system:config'] + else: + data = {} + return data + + def get_naming(self): + """Get interface_naming type available in chassis""" + request = [{"path": "data/sonic-device-metadata:sonic-device-metadata/DEVICE_METADATA/DEVICE_METADATA_LIST=localhost", "method": GET}] + 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 ('sonic-device-metadata:DEVICE_METADATA_LIST' in response[0][1]): + intf_data = response[0][1]['sonic-device-metadata:DEVICE_METADATA_LIST'] + if 'intf_naming_mode' in intf_data[0]: + data = intf_data[0] + else: + data = {} + return data + + def get_anycast_addr(self): + """Get system anycast address available in chassis""" + request = [{"path": "data/sonic-sag:sonic-sag/SAG_GLOBAL/SAG_GLOBAL_LIST/", "method": GET}] + 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 ('sonic-sag:SAG_GLOBAL_LIST' in response[0][1]): + data = response[0][1]['sonic-sag:SAG_GLOBAL_LIST'][0] + else: + data = {} + return data + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for system + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_system() + intf_naming = self.get_naming() + if intf_naming: + data.update(intf_naming) + anycast_addr = self.get_anycast_addr() + if anycast_addr: + data.update(anycast_addr) + objs = [] + objs = self.render_config(self.generated_spec, data) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['system'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = self.parse_sonic_system(spec, conf) + return config + + def parse_sonic_system(self, spec, conf): + config = deepcopy(spec) + if conf: + if ('hostname' in conf) and (conf['hostname']): + config['hostname'] = conf['hostname'] + if ('intf_naming_mode' in conf) and (conf['intf_naming_mode']): + config['interface_naming'] = conf['intf_naming_mode'] + if ('IPv4' in conf) and (conf['IPv4'] == "enable"): + config['anycast_address']['ipv4'] = True + if ('IPv4' in conf) and (conf['IPv4'] == "disable"): + config['anycast_address']['ipv4'] = False + if ('IPv6' in conf) and (conf['IPv6'] == "enable"): + config['anycast_address']['ipv6'] = True + if ('IPv6' in conf) and (conf['IPv6'] == "disable"): + config['anycast_address']['ipv6'] = False + if ('gwmac' in conf) and (conf['gwmac']): + config['anycast_address']['mac_address'] = conf['gwmac'] + return utils.remove_empties(config) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/tacacs_server/tacacs_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/tacacs_server/tacacs_server.py new file mode 100644 index 000000000..a1e79910f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/tacacs_server/tacacs_server.py @@ -0,0 +1,150 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic tacas server 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 +import re +import json +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.tacacs_server.tacacs_server import Tacacs_serverArgs +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 Tacacs_serverFacts(object): + """ The sonic tacas server fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Tacacs_serverArgs.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 tacacs_server + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + obj = None + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_tacacs_server() + + obj = self.render_config(self.generated_spec, data) + + ansible_facts['ansible_network_resources'].pop('tacacs_server', None) + facts = {} + if obj: + facts['tacacs_server'] = {} + params = utils.validate_config(self.argument_spec, {'config': obj}) + if params: + facts['tacacs_server'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def get_all_tacacs_server(self): + """Get all the tacacs_server configured in the device""" + request = [{"path": "data/openconfig-system:system/aaa/server-groups/server-group=TACACS/config", "method": GET}] + tacacs_server_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:config" in response[0][1]: + raw_tacacs_global_data = response[0][1].get("openconfig-system:config", {}) + + if 'auth-type' in raw_tacacs_global_data: + tacacs_server_data['auth_type'] = raw_tacacs_global_data['auth-type'] + if 'secret-key' in raw_tacacs_global_data: + tacacs_server_data['key'] = raw_tacacs_global_data['secret-key'] + if 'source-interface' in raw_tacacs_global_data: + tacacs_server_data['source_interface'] = raw_tacacs_global_data['source-interface'] + if 'timeout' in raw_tacacs_global_data: + tacacs_server_data['timeout'] = raw_tacacs_global_data['timeout'] + + request = [{"path": "data/openconfig-system:system/aaa/server-groups/server-group=TACACS/servers", "method": GET}] + hosts = [] + 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) + + raw_tacacs_server_list = [] + if "openconfig-system:servers" in response[0][1]: + raw_tacacs_server_list = response[0][1].get("openconfig-system:servers", {}).get('server', []) + + for tacacs_host in raw_tacacs_server_list: + host_data = {} + if 'address' in tacacs_host: + host_data['name'] = tacacs_host['address'] + cfg = tacacs_host.get('config', None) + if cfg: + if 'auth-type' in cfg: + host_data['auth_type'] = cfg['auth-type'] + if 'priority' in cfg: + host_data['priority'] = cfg['priority'] + if 'vrf' in cfg: + host_data['vrf'] = cfg['vrf'] + if 'timeout' in cfg: + host_data['timeout'] = cfg['timeout'] + if tacacs_host.get('tacacs', None) and tacacs_host['tacacs'].get('config', None): + tacas_cfg = tacacs_host['tacacs']['config'] + if tacas_cfg.get('port', None): + host_data['port'] = tacas_cfg['port'] + if tacas_cfg.get('secret-key', None): + host_data['key'] = tacas_cfg['secret-key'] + if host_data: + hosts.append(host_data) + + if hosts: + tacacs_server_data['servers'] = {'host': hosts} + + return tacacs_server_data diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/users/users.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/users/users.py new file mode 100644 index 000000000..038e97f83 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/users/users.py @@ -0,0 +1,122 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic users 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 +import re +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.users.users import UsersArgs +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 UsersFacts(object): + """ The sonic users fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = UsersArgs.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 users + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_users() + + objs = list() + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('users', None) + facts = {} + if objs: + facts['users'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + if params: + facts['users'].extend(params['config']) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def get_all_users(self): + """Get all the users configured in the device""" + request = [{"path": "data/sonic-system-aaa:sonic-system-aaa/USER", "method": GET}] + users = [] + 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) + + raw_users = [] + if "sonic-system-aaa:USER" in response[0][1]: + raw_users = response[0][1].get("sonic-system-aaa:USER", {}).get('USER_LIST', []) + + for raw_user in raw_users: + name = raw_user.get('username', None) + role = raw_user.get('role', []) + if role and len(role) > 0: + role = role[0] + password = raw_user.get('password', None) + user = {} + if name and role: + user['name'] = name + user['role'] = role + if password: + user['password'] = password + if user: + users.append(user) + return users diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlans/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlans/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlans/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlans/vlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlans/vlans.py new file mode 100644 index 000000000..7c4af2ea8 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlans/vlans.py @@ -0,0 +1,126 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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 vlans 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 + +import re +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.vlans.vlans import VlansArgs +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 VlansFacts(object): + """ The sonic vlans fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = VlansArgs.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 vlans + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + vlans = self.get_vlans() + objs = [] + for vlan_id, vlan_config in vlans.items(): + obj = self.render_config(self.generated_spec, vlan_config) + if obj: + objs.append(obj) + ansible_facts['ansible_network_resources'].pop('vlans', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['vlans'] = params['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + config = deepcopy(spec) + try: + config['vlan_id'] = int(conf['vlan_id']) + if conf.get('description', None): + config['description'] = conf['description'] + except TypeError: + config['vlan_id'] = None + config['description'] = None + return utils.remove_empties(config) + + def get_vlans(self): + """Get all the l2_interfaces available in chassis""" + request = [{"path": "data/openconfig-interfaces:interfaces", "method": GET}] + 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) + + interfaces = {} + if "openconfig-interfaces:interfaces" in response[0][1]: + interfaces = response[0][1].get("openconfig-interfaces:interfaces", {}) + if interfaces.get("interface"): + interfaces = interfaces['interface'] + + ret_vlan_configs = {} + + for interface in interfaces: + interface_name = interface.get("config").get("name") + description = interface.get("config").get("description", None) + if "Vlan" in interface_name: + vlan_id = interface_name.split("Vlan")[1] + vlan_configs = {"vlan_id": vlan_id, + "name": interface_name, + } + if description: + vlan_configs['description'] = description + + ret_vlan_configs.update({vlan_id: vlan_configs}) + + return ret_vlan_configs diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vrfs/vrfs.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vrfs/vrfs.py new file mode 100644 index 000000000..797612bc4 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vrfs/vrfs.py @@ -0,0 +1,120 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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 vrfs 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 + +import re +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.vrfs.vrfs import VrfsArgs +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 VrfsFacts(object): + """ The sonic vrfs fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = VrfsArgs.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 vrf + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_vrf_interfaces() + + objs = list() + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('vrfs', None) + facts = {} + if objs: + facts['vrfs'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + if params: + facts['vrfs'].extend(params['config']) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def get_all_vrf_interfaces(self): + """Get all the interfaces available in chassis""" + all_network_instatnces = {} + request = [{"path": "data/openconfig-network-instance:network-instances", "method": GET}] + 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-network-instance:network-instances" in response[0][1]: + all_network_instatnces = response[0][1].get("openconfig-network-instance:network-instances", {}) + return self.get_vrf_interfaces_from_network_instances(all_network_instatnces['network-instance']) + + def get_vrf_interfaces_from_network_instances(self, network_instances): + vrf_interfaces = [] + + for each_ins in network_instances: + vrf_interface = dict() + name = each_ins['name'] + if name.startswith('Vrf') or name == 'mgmt': + vrf_interface['name'] = name + if each_ins.get("interfaces"): + interfaces = [{"name": intf.get("id")} for intf in each_ins["interfaces"]["interface"]] + vrf_interface["members"] = {"interfaces": interfaces} + + vrf_interfaces.append(vrf_interface) + return vrf_interfaces diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vxlans/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vxlans/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vxlans/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vxlans/vxlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vxlans/vxlans.py new file mode 100644 index 000000000..51aec6561 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vxlans/vxlans.py @@ -0,0 +1,207 @@ +# +# -*- coding: utf-8 -*- +# © Copyright 2020 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 vxlans 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 + +import re +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.vxlans.vxlans import VxlansArgs +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 VxlansFacts(object): + """ The sonic vxlans fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = VxlansArgs.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 vxlans + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if connection: # just for linting purposes, remove + pass + + if not data: + # typically data is populated from the current device configuration + # data = connection.get('show running-config | section ^interface') + # using mock data instead + data = self.get_all_vxlans() + + objs = list() + for conf in data: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('vxlans', None) + facts = {} + if objs: + facts['vxlans'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + if params: + facts['vxlans'].extend(params['config']) + ansible_facts['ansible_network_resources'].update(facts) + + return ansible_facts + + def render_config(self, spec, conf): + """ + Render config as dictionary structure and delete keys + from spec for null values + + :param spec: The facts tree, generated from the argspec + :param conf: The configuration + :rtype: dictionary + :returns: The generated config + """ + return conf + + def get_all_vxlans(self): + vxlans = [] + vxlan_tunnels = [] + vxlan_vlan_map = [] + + vxlans_tunnels_vlan_map = self.get_all_vxlans_tunnels_vlan_map() + vxlans_evpn_nvo_list = self.get_all_vxlans_evpn_nvo_list() + + if vxlans_tunnels_vlan_map.get('VXLAN_TUNNEL'): + if vxlans_tunnels_vlan_map['VXLAN_TUNNEL'].get('VXLAN_TUNNEL_LIST'): + vxlan_tunnels.extend(vxlans_tunnels_vlan_map['VXLAN_TUNNEL']['VXLAN_TUNNEL_LIST']) + + if vxlans_tunnels_vlan_map.get('VXLAN_TUNNEL_MAP'): + if vxlans_tunnels_vlan_map['VXLAN_TUNNEL_MAP'].get('VXLAN_TUNNEL_MAP_LIST'): + vxlan_vlan_map.extend(vxlans_tunnels_vlan_map['VXLAN_TUNNEL_MAP']['VXLAN_TUNNEL_MAP_LIST']) + + self.fill_tunnel_source_ip(vxlans, vxlan_tunnels, vxlans_evpn_nvo_list) + self.fill_vlan_map(vxlans, vxlan_vlan_map) + + vxlan_vrf_list = self.get_all_vxlans_vrf_list() + self.fill_vrf_map(vxlans, vxlan_vrf_list) + + return vxlans + + def get_all_vxlans_vrf_list(self): + """Get all the vxlan tunnels and vlan map available """ + request = [{"path": "data/sonic-vrf:sonic-vrf/VRF/VRF_LIST", "method": GET}] + 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 "sonic-vrf:VRF_LIST" in response[0][1]: + vxlan_vrf_list = response[0][1].get("sonic-vrf:VRF_LIST", {}) + + return vxlan_vrf_list + + def get_all_vxlans_evpn_nvo_list(self): + """Get all the evpn nvo list available """ + request = [{"path": "data/sonic-vxlan:sonic-vxlan/EVPN_NVO/EVPN_NVO_LIST", "method": GET}] + 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) + + vxlans_evpn_nvo_list = [] + if "sonic-vxlan:EVPN_NVO_LIST" in response[0][1]: + vxlans_evpn_nvo_list = response[0][1].get("sonic-vxlan:EVPN_NVO_LIST", []) + + return vxlans_evpn_nvo_list + + def get_all_vxlans_tunnels_vlan_map(self): + """Get all the vxlan tunnels and vlan map available """ + request = [{"path": "data/sonic-vxlan:sonic-vxlan", "method": GET}] + 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) + + vxlans_tunnels_vlan_map = {} + if "sonic-vxlan:sonic-vxlan" in response[0][1]: + vxlans_tunnels_vlan_map = response[0][1].get("sonic-vxlan:sonic-vxlan", {}) + + return vxlans_tunnels_vlan_map + + def fill_tunnel_source_ip(self, vxlans, vxlan_tunnels, vxlans_evpn_nvo_list): + for each_tunnel in vxlan_tunnels: + vxlan = dict() + vxlan['name'] = each_tunnel['name'] + vxlan['source_ip'] = each_tunnel.get('src_ip', None) + vxlan['primary_ip'] = each_tunnel.get('primary_ip', None) + vxlan['evpn_nvo'] = None + if vxlan['source_ip']: + evpn_nvo = next((nvo_map['name'] for nvo_map in vxlans_evpn_nvo_list if nvo_map['source_vtep'] == vxlan['name']), None) + if evpn_nvo: + vxlan['evpn_nvo'] = evpn_nvo + vxlans.append(vxlan) + + def fill_vlan_map(self, vxlans, vxlan_vlan_map): + for each_vlan_map in vxlan_vlan_map: + name = each_vlan_map['name'] + matched_vtep = next((each_vxlan for each_vxlan in vxlans if each_vxlan['name'] == name), None) + if matched_vtep: + vni = int(each_vlan_map['vni']) + vlan = int(each_vlan_map['vlan'][4:]) + vlan_map = matched_vtep.get('vlan_map') + if vlan_map: + vlan_map.append(dict({'vni': vni, 'vlan': vlan})) + else: + matched_vtep['vlan_map'] = [dict({'vni': vni, 'vlan': vlan})] + + def fill_vrf_map(self, vxlans, vxlan_vrf_list): + for each_vrf in vxlan_vrf_list: + vni = each_vrf.get('vni', None) + if vni is None: + continue + + matched_vtep = None + for each_vxlan in vxlans: + for each_vlan in each_vxlan.get('vlan_map', []): + if vni == each_vlan['vni']: + matched_vtep = each_vxlan + + if matched_vtep: + vni = int(each_vrf['vni']) + vrf = each_vrf['vrf_name'] + vrf_map = matched_vtep.get('vrf_map') + if vrf_map: + vrf_map.append(dict({'vni': vni, 'vrf': vrf})) + else: + matched_vtep['vrf_map'] = [dict({'vni': vni, 'vrf': vrf})] diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/sonic.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/sonic.py new file mode 100644 index 000000000..77a63d425 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/sonic.py @@ -0,0 +1,155 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Modules you write using this snippet, which is embedded dynamically by Ansible +# still belong to the author of the module, and may assign their own license +# to the complete work. +# +# (c) 2016 Red Hat Inc. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import json +import re + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import env_fallback +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, + ComplexList +) +from ansible.module_utils.connection import Connection, ConnectionError +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig, ConfigLine + +_DEVICE_CONFIGS = {} +STANDARD_ETH_REGEXP = r"Eth\d+(/\d+)+" +PATTERN = re.compile(STANDARD_ETH_REGEXP) + + +def get_connection(module): + if hasattr(module, "_sonic_connection"): + return module._sonic_connection + + capabilities = get_capabilities(module) + network_api = capabilities.get("network_api") + if network_api in ["cliconf", "sonic_rest"]: + module._sonic_connection = Connection(module._socket_path) + else: + module.fail_json(msg="Invalid connection type %s" % network_api) + + return module._sonic_connection + + +def get_capabilities(module): + if hasattr(module, "_sonic_capabilities"): + return module._sonic_capabilities + try: + capabilities = Connection(module._socket_path).get_capabilities() + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + module._sonic_capabilities = json.loads(capabilities) + return module._sonic_capabilities + + +def get_config(module, flags=None): + flags = to_list(flags) + flag_str = " ".join(flags) + + try: + return _DEVICE_CONFIGS[flag_str] + except KeyError: + connection = get_connection(module) + try: + out = connection.get_config(flags=flags) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + cfg = to_text(out, errors="surrogate_then_replace").strip() + _DEVICE_CONFIGS[flag_str] = cfg + return cfg + + +def get_sublevel_config(running_config, module): + contents = list() + current_config_contents = list() + running_config = NetworkConfig(contents=running_config, indent=1) + obj = running_config.get_object(module.params['parents']) + if obj: + contents = obj.children + parents = module.params['parents'] + if parents[2:]: + temp = 1 + for count, item in enumerate(parents[2:], start=2): + item = ' ' * temp + item + temp = temp + 1 + parents[count] = item + contents[:0] = parents + indent = 0 + for c in contents: + if isinstance(c, str): + if c in parents: + current_config_contents.append(c.rjust(len(c) + indent, ' ')) + if c not in parents: + c = ' ' * (len(parents) - 1) + c + current_config_contents.append(c.rjust(len(c) + indent, ' ')) + if isinstance(c, ConfigLine): + current_config_contents.append(c.raw) + indent = 1 + sublevel_config = '\n'.join(current_config_contents) + return sublevel_config + + +def run_commands(module, commands, check_rc=True): + connection = get_connection(module) + try: + return connection.run_commands(commands=commands, check_rc=check_rc) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) + + +def edit_config(module, commands, skip_code=None): + connection = get_connection(module) + + # Start: This is to convert interface name from Eth1/1 to Eth1%2f1 + for request in commands: + # This check is to differenciate between requests and commands + if type(request) is dict: + url = request.get("path", None) + if url: + request["path"] = update_url(url) + # End + return connection.edit_config(commands) + + +def update_url(url): + match = re.search(STANDARD_ETH_REGEXP, url) + ret_url = url + if match: + interface_name = match.group() + interface_name = interface_name.replace("/", "%2f") + ret_url = PATTERN.sub(interface_name, url) + return ret_url + + +def to_request(module, requests): + transform = ComplexList(dict(path=dict(key=True), method=dict(), data=dict(type='dict')), module) + return transform(to_list(requests)) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/bgp_utils.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/bgp_utils.py new file mode 100644 index 000000000..7471bcb11 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/bgp_utils.py @@ -0,0 +1,611 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic bgp 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 + +import re +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.utils.utils import ( + normalize_interface_name, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.bgp.bgp import BgpArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + +afi_safi_types_map = { + 'openconfig-bgp-types:IPV4_UNICAST': 'ipv4_unicast', + 'openconfig-bgp-types:IPV6_UNICAST': 'ipv6_unicast', + 'openconfig-bgp-types:L2VPN_EVPN': 'l2vpn_evpn', +} +GET = "get" +network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' +protocol_bgp_path = 'protocols/protocol=BGP,bgp/bgp' + + +def get_all_vrfs(module): + """Get all VRF configurations available in chassis""" + all_vrfs = [] + ret = [] + request = {"path": "data/sonic-vrf:sonic-vrf/VRF/VRF_LIST", "method": GET} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + if 'sonic-vrf:VRF_LIST' in response[0][1]: + all_vrf_data = response[0][1].get('sonic-vrf:VRF_LIST', []) + if all_vrf_data: + for vrf_data in all_vrf_data: + all_vrfs.append(vrf_data['vrf_name']) + + return all_vrfs + + +def get_peergroups(module, vrf_name): + peer_groups = [] + request_path = '%s=%s/protocols/protocol=BGP,bgp/bgp/peer-groups' % (network_instance_path, vrf_name) + request = {"path": request_path, "method": GET} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + resp = response[0][1] + if 'openconfig-network-instance:peer-groups' in resp: + data = resp['openconfig-network-instance:peer-groups'] + if 'peer-group' in data: + for peer_group in data['peer-group']: + pg = {} + if 'config' in peer_group: + if 'peer-group-name' in peer_group['config']: + pg.update({'name': peer_group['config']['peer-group-name']}) + if 'description' in peer_group['config']: + pg.update({'pg_description': peer_group['config']['description']}) + if 'disable-ebgp-connected-route-check' in peer_group['config']: + pg.update({'disable_connected_check': peer_group['config']['disable-ebgp-connected-route-check']}) + if 'dont-negotiate-capability' in peer_group['config']: + pg.update({'dont_negotiate_capability': peer_group['config']['dont-negotiate-capability']}) + if 'enforce-first-as' in peer_group['config']: + pg.update({'enforce_first_as': peer_group['config']['enforce-first-as']}) + if 'enforce-multihop' in peer_group['config']: + pg.update({'enforce_multihop': peer_group['config']['enforce-multihop']}) + local_as = {} + if 'local-as' in peer_group['config']: + local_as.update({'as': peer_group['config']['local-as']}) + if 'local-as-no-prepend' in peer_group['config']: + local_as.update({'no_prepend': peer_group['config']['local-as-no-prepend']}) + if 'local-as-replace-as' in peer_group['config']: + local_as.update({'replace_as': peer_group['config']['local-as-replace-as']}) + if 'override-capability' in peer_group['config']: + pg.update({'override_capability': peer_group['config']['override-capability']}) + if 'shutdown-message' in peer_group['config']: + pg.update({'shutdown_msg': peer_group['config']['shutdown-message']}) + if 'solo-peer' in peer_group['config']: + pg.update({'solo': peer_group['config']['solo-peer']}) + if 'strict-capability-match' in peer_group['config']: + pg.update({'strict_capability_match': peer_group['config']['strict-capability-match']}) + if 'ttl-security-hops' in peer_group['config']: + pg.update({'ttl_security': peer_group['config']['ttl-security-hops']}) + auth_pwd = {} + if 'auth-password' in peer_group and 'config' in peer_group['auth-password']: + if 'encrypted' in peer_group['auth-password']['config']: + auth_pwd.update({'encrypted': peer_group['auth-password']['config']['encrypted']}) + if 'password' in peer_group['auth-password']['config']: + auth_pwd.update({'pwd': peer_group['auth-password']['config']['password']}) + bfd = {} + if 'enable-bfd' in peer_group and 'config' in peer_group['enable-bfd']: + if 'enabled' in peer_group['enable-bfd']['config']: + bfd.update({'enabled': peer_group['enable-bfd']['config']['enabled']}) + if 'check-control-plane-failure' in peer_group['enable-bfd']['config']: + bfd.update({'check_failure': peer_group['enable-bfd']['config']['check-control-plane-failure']}) + if 'bfd-profile' in peer_group['enable-bfd']['config']: + bfd.update({'profile': peer_group['enable-bfd']['config']['bfd-profile']}) + ebgp_multihop = {} + if 'ebgp-multihop' in peer_group and 'config' in peer_group['ebgp-multihop']: + if 'enabled' in peer_group['ebgp-multihop']['config']: + ebgp_multihop.update({'enabled': peer_group['ebgp-multihop']['config']['enabled']}) + if 'multihop-ttl' in peer_group['ebgp-multihop']['config']: + ebgp_multihop.update({'multihop_ttl': peer_group['ebgp-multihop']['config']['multihop-ttl']}) + if 'transport' in peer_group and 'config' in peer_group['transport']: + if 'local-address' in peer_group['transport']['config']: + pg.update({'local_address': peer_group['transport']['config']['local-address']}) + if 'passive-mode' in peer_group['transport']['config']: + pg.update({'passive': peer_group['transport']['config']['passive-mode']}) + if 'timers' in peer_group and 'config' in peer_group['timers']: + if 'minimum-advertisement-interval' in peer_group['timers']['config']: + pg.update({'advertisement_interval': peer_group['timers']['config']['minimum-advertisement-interval']}) + timers = {} + if 'hold-time' in peer_group['timers']['config']: + timers.update({'holdtime': peer_group['timers']['config']['hold-time']}) + if 'keepalive-interval' in peer_group['timers']['config']: + timers.update({'keepalive': peer_group['timers']['config']['keepalive-interval']}) + if 'connect-retry' in peer_group['timers']['config']: + timers.update({'connect_retry': peer_group['timers']['config']['connect-retry']}) + capability = {} + if 'config' in peer_group and 'capability-dynamic' in peer_group['config']: + capability.update({'dynamic': peer_group['config']['capability-dynamic']}) + if 'config' in peer_group and 'capability-extended-nexthop' in peer_group['config']: + capability.update({'extended_nexthop': peer_group['config']['capability-extended-nexthop']}) + remote_as = {} + if 'config' in peer_group and 'peer-as' in peer_group['config']: + remote_as.update({'peer_as': peer_group['config']['peer-as']}) + if 'config' in peer_group and 'peer-type' in peer_group['config']: + remote_as.update({'peer_type': peer_group['config']['peer-type'].lower()}) + afis = [] + if 'afi-safis' in peer_group and 'afi-safi' in peer_group['afi-safis']: + for each in peer_group['afi-safis']['afi-safi']: + samp = {} + if 'afi-safi-name' in each and each['afi-safi-name']: + tmp = each['afi-safi-name'].split(':') + if tmp: + split_tmp = tmp[1].split('_') + if split_tmp: + afi = split_tmp[0].lower() + safi = split_tmp[1].lower() + if afi and safi: + samp.update({'afi': afi}) + samp.update({'safi': safi}) + if 'config' in each and 'enabled' in each['config']: + samp.update({'activate': each['config']['enabled']}) + if 'allow-own-as' in each and 'config' in each['allow-own-as']: + allowas_in = {} + allowas_conf = each['allow-own-as']['config'] + if 'origin' in allowas_conf and allowas_conf['origin']: + allowas_in.update({'origin': allowas_conf['origin']}) + elif 'as-count' in allowas_conf and allowas_conf['as-count']: + allowas_in.update({'value': allowas_conf['as-count']}) + if allowas_in: + samp.update({'allowas_in': allowas_in}) + if 'ipv4-unicast' in each: + if 'config' in each['ipv4-unicast']: + ip_afi_conf = each['ipv4-unicast']['config'] + ip_afi = update_bgp_nbr_pg_ip_afi_dict(ip_afi_conf) + if ip_afi: + samp.update({'ip_afi': ip_afi}) + if 'prefix-limit' in each['ipv4-unicast'] and 'config' in each['ipv4-unicast']['prefix-limit']: + pfx_lmt_conf = each['ipv4-unicast']['prefix-limit']['config'] + prefix_limit = update_bgp_nbr_pg_prefix_limit_dict(pfx_lmt_conf) + if prefix_limit: + samp.update({'prefix_limit': prefix_limit}) + elif 'ipv6-unicast' in each: + if 'config' in each['ipv6-unicast']: + ip_afi_conf = each['ipv6-unicast']['config'] + ip_afi = update_bgp_nbr_pg_ip_afi_dict(ip_afi_conf) + if ip_afi: + samp.update({'ip_afi': ip_afi}) + if 'prefix-limit' in each['ipv6-unicast'] and 'config' in each['ipv6-unicast']['prefix-limit']: + pfx_lmt_conf = each['ipv6-unicast']['prefix-limit']['config'] + prefix_limit = update_bgp_nbr_pg_prefix_limit_dict(pfx_lmt_conf) + if prefix_limit: + samp.update({'prefix_limit': prefix_limit}) + elif 'l2vpn-evpn' in each and 'prefix-limit' in each['l2vpn-evpn'] and 'config' in each['l2vpn-evpn']['prefix-limit']: + pfx_lmt_conf = each['l2vpn-evpn']['prefix-limit']['config'] + prefix_limit = update_bgp_nbr_pg_prefix_limit_dict(pfx_lmt_conf) + if prefix_limit: + samp.update({'prefix_limit': prefix_limit}) + if 'prefix-list' in each and 'config' in each['prefix-list']: + pfx_lst_conf = each['prefix-list']['config'] + if 'import-policy' in pfx_lst_conf and pfx_lst_conf['import-policy']: + samp.update({'prefix_list_in': pfx_lst_conf['import-policy']}) + if 'export-policy' in pfx_lst_conf and pfx_lst_conf['export-policy']: + samp.update({'prefix_list_out': pfx_lst_conf['export-policy']}) + if samp: + afis.append(samp) + if auth_pwd: + pg.update({'auth_pwd': auth_pwd}) + if bfd: + pg.update({'bfd': bfd}) + if ebgp_multihop: + pg.update({'ebgp_multihop': ebgp_multihop}) + if local_as: + pg.update({'local_as': local_as}) + if timers: + pg.update({'timers': timers}) + if capability: + pg.update({'capability': capability}) + if remote_as: + pg.update({'remote_as': remote_as}) + if afis and len(afis) > 0: + afis_dict = {} + afis_dict.update({'afis': afis}) + pg.update({'address_family': afis_dict}) + peer_groups.append(pg) + + return peer_groups + + +def update_bgp_nbr_pg_ip_afi_dict(ip_afi_conf): + ip_afi = {} + if 'default-policy-name' in ip_afi_conf and ip_afi_conf['default-policy-name']: + ip_afi.update({'default_policy_name': ip_afi_conf['default-policy-name']}) + if 'send-default-route' in ip_afi_conf and ip_afi_conf['send-default-route']: + ip_afi.update({'send_default_route': ip_afi_conf['send-default-route']}) + + return ip_afi + + +def update_bgp_nbr_pg_prefix_limit_dict(pfx_lmt_conf): + prefix_limit = {} + if 'max-prefixes' in pfx_lmt_conf and pfx_lmt_conf['max-prefixes']: + prefix_limit.update({'max_prefixes': pfx_lmt_conf['max-prefixes']}) + if 'prevent-teardown' in pfx_lmt_conf and pfx_lmt_conf['prevent-teardown']: + prefix_limit.update({'prevent_teardown': pfx_lmt_conf['prevent-teardown']}) + if 'warning-threshold-pct' in pfx_lmt_conf and pfx_lmt_conf['warning-threshold-pct']: + prefix_limit.update({'warning_threshold': pfx_lmt_conf['warning-threshold-pct']}) + if 'restart-timer' in pfx_lmt_conf and pfx_lmt_conf['restart-timer']: + prefix_limit.update({'restart_timer': pfx_lmt_conf['restart-timer']}) + + return prefix_limit + + +def get_ip_afi_cfg_payload(ip_afi): + ip_afi_cfg = {} + + if ip_afi.get('default_policy_name', None) is not None: + default_policy_name = ip_afi['default_policy_name'] + ip_afi_cfg.update({'default-policy-name': default_policy_name}) + if ip_afi.get('send_default_route', None) is not None: + send_default_route = ip_afi['send_default_route'] + ip_afi_cfg.update({'send-default-route': send_default_route}) + + return ip_afi_cfg + + +def get_prefix_limit_payload(prefix_limit): + pfx_lmt_cfg = {} + + if prefix_limit.get('max_prefixes', None) is not None: + max_prefixes = prefix_limit['max_prefixes'] + pfx_lmt_cfg.update({'max-prefixes': max_prefixes}) + if prefix_limit.get('prevent_teardown', None) is not None: + prevent_teardown = prefix_limit['prevent_teardown'] + pfx_lmt_cfg.update({'prevent-teardown': prevent_teardown}) + if prefix_limit.get('warning_threshold', None) is not None: + warning_threshold = prefix_limit['warning_threshold'] + pfx_lmt_cfg.update({'warning-threshold-pct': warning_threshold}) + if prefix_limit.get('restart_timer', None) is not None: + restart_timer = prefix_limit['restart_timer'] + pfx_lmt_cfg.update({'restart-timer': restart_timer}) + + return pfx_lmt_cfg + + +def get_all_bgp_af_redistribute(module, vrfs, af_redis_params_map): + """Get all BGP Global Address Family Redistribute configurations available in chassis""" + all_af_redis_data = [] + ret_redis_data = [] + for vrf_name in vrfs: + af_redis_data = {} + request_path = '%s=%s/table-connections' % (network_instance_path, vrf_name) + request = {"path": request_path, "method": GET} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + if "openconfig-network-instance:table-connections" in response[0][1]: + af_redis_data.update({vrf_name: response[0][1]['openconfig-network-instance:table-connections']}) + + if af_redis_data: + all_af_redis_data.append(af_redis_data) + + if all_af_redis_data: + for vrf_name in vrfs: + key = vrf_name + val = next((af_redis_data for af_redis_data in all_af_redis_data if vrf_name in af_redis_data), None) + if not val: + continue + + val = val[vrf_name] + redis_data = val.get('table-connection', []) + if not redis_data: + continue + filtered_redis_data = [] + for e_cfg in redis_data: + af_redis_data = get_from_params_map(af_redis_params_map, e_cfg) + if af_redis_data: + filtered_redis_data.append(af_redis_data) + + if filtered_redis_data: + ret_redis_data.append({key: filtered_redis_data}) + + return ret_redis_data + + +def get_all_bgp_globals(module, vrfs): + """Get all BGP configurations available in chassis""" + all_bgp_globals = [] + for vrf_name in vrfs: + get_path = '%s=%s/%s/global' % (network_instance_path, vrf_name, protocol_bgp_path) + request = {"path": get_path, "method": GET} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + for resp in response: + if "openconfig-network-instance:global" in resp[1]: + bgp_data = {'global': resp[1].get("openconfig-network-instance:global", {})} + bgp_data.update({'vrf_name': vrf_name}) + all_bgp_globals.append(bgp_data) + return all_bgp_globals + + +def get_bgp_global_af_data(data, af_params_map): + ret_af_data = {} + for key, val in data.items(): + if key == 'global': + if 'afi-safis' in val and 'afi-safi' in val['afi-safis']: + global_af_data = [] + raw_af_data = val['afi-safis']['afi-safi'] + for each_af_data in raw_af_data: + af_data = get_from_params_map(af_params_map, each_af_data) + if af_data: + global_af_data.append(af_data) + ret_af_data.update({'address_family': global_af_data}) + if 'config' in val and 'as' in val['config']: + as_val = val['config']['as'] + ret_af_data.update({'bgp_as': as_val}) + if key == 'vrf_name': + ret_af_data.update({'vrf_name': val}) + return ret_af_data + + +def get_bgp_global_data(data, global_params_map): + bgp_data = {} + for key, val in data.items(): + if key == 'global': + global_data = get_from_params_map(global_params_map, val) + bgp_data.update(global_data) + if key == 'vrf_name': + bgp_data.update({'vrf_name': val}) + return bgp_data + + +def get_from_params_map(params_map, data): + ret_data = {} + for want_key, config_key in params_map.items(): + tmp_data = {} + for key, val in data.items(): + if key == 'config': + for k, v in val.items(): + if k == config_key: + val_data = val[config_key] + ret_data.update({want_key: val_data}) + if config_key == 'afi-safi-name': + ret_data.pop(want_key) + for type_k, type_val in afi_safi_types_map.items(): + if type_k == val_data: + afi_safi = type_val.split('_') + val_data = afi_safi[0] + ret_data.update({'safi': afi_safi[1]}) + ret_data.update({want_key: val_data}) + break + else: + if key == 'timers' and ('config' in val or 'state' in val): + tmp = {} + if key in ret_data: + tmp = ret_data[key] + cfg = val['config'] if 'config' in val else val['state'] + for k, v in cfg.items(): + if k == config_key: + if k != 'minimum-advertisement-interval': + tmp.update({want_key: cfg[config_key]}) + else: + ret_data.update({want_key: cfg[config_key]}) + if tmp: + ret_data.update({key: tmp}) + + elif isinstance(config_key, list): + i = 0 + if key == config_key[0]: + if key == 'afi-safi': + cfg_data = config_key[1] + for itm in afi_safi_types_map: + if cfg_data in itm: + afi_safi = itm[cfg_data].split('_') + cfg_data = afi_safi[0] + ret_data.update({'safi': afi_safi[1]}) + ret_data.update({want_key: cfg_data}) + break + else: + cfg_data = {key: val} + for cfg_key in config_key: + if cfg_key == 'config': + continue + new_data = None + + if cfg_key in cfg_data: + new_data = cfg_data[cfg_key] + elif isinstance(cfg_data, dict) and 'config' in cfg_data: + if cfg_key in cfg_data['config']: + new_data = cfg_data['config'][cfg_key] + + if new_data is not None: + cfg_data = new_data + else: + break + else: + ret_data.update({want_key: cfg_data}) + else: + if key == config_key and val: + if config_key != 'afi-safi-name' and config_key != 'timers': + cfg_data = val + ret_data.update({want_key: cfg_data}) + + return ret_data + + +def get_bgp_data(module, global_params_map): + vrf_list = get_all_vrfs(module) + data = get_all_bgp_globals(module, vrf_list) + + objs = [] + # operate on a collection of resource x + for conf in data: + if conf: + obj = get_bgp_global_data(conf, global_params_map) + if obj: + objs.append(obj) + return objs + + +def get_bgp_af_data(module, af_params_map): + vrf_list = get_all_vrfs(module) + data = get_all_bgp_globals(module, vrf_list) + + objs = [] + # operate on a collection of resource x + for conf in data: + if conf: + obj = get_bgp_global_af_data(conf, af_params_map) + if obj: + objs.append(obj) + + return objs + + +def get_bgp_as(module, vrf_name): + as_val = None + get_path = '%s=%s/%s/global/config' % (network_instance_path, vrf_name, protocol_bgp_path) + request = {"path": get_path, "method": GET} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + resp = response[0][1] + if "openconfig-network-instance:config" in resp and 'as' in resp['openconfig-network-instance:config']: + as_val = resp['openconfig-network-instance:config']['as'] + return as_val + + +def get_bgp_neighbors(module, vrf_name): + neighbors_data = None + get_path = '%s=%s/%s/neighbors' % (network_instance_path, vrf_name, protocol_bgp_path) + request = {"path": get_path, "method": GET} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + resp = response[0][1] + if "openconfig-network-instance:neighbors" in resp: + neighbors_data = resp['openconfig-network-instance:neighbors'] + + return neighbors_data + + +def get_all_bgp_neighbors(module): + vrf_list = get_all_vrfs(module) + """Get all BGP neighbor configurations available in chassis""" + all_bgp_neighbors = [] + + for vrf_name in vrf_list: + neighbors_cfg = {} + + bgp_as = get_bgp_as(module, vrf_name) + if bgp_as: + neighbors_cfg['bgp_as'] = bgp_as + neighbors_cfg['vrf_name'] = vrf_name + else: + continue + + neighbors = get_bgp_neighbors(module, vrf_name) + if neighbors: + neighbors_cfg['neighbors'] = neighbors + + if neighbors_cfg: + all_bgp_neighbors.append(neighbors_cfg) + + return all_bgp_neighbors + + +def get_undefined_bgps(want, have, check_neighbors=None): + if check_neighbors is None: + check_neighbors = False + + undefined_resources = [] + + if not want: + return undefined_resources + + if not have: + have = [] + + for want_conf in want: + undefined = {} + want_bgp_as = want_conf['bgp_as'] + want_vrf = want_conf['vrf_name'] + have_conf = next((conf for conf in have if (want_bgp_as == conf['bgp_as'] and want_vrf == conf['vrf_name'])), None) + if not have_conf: + undefined['bgp_as'] = want_bgp_as + undefined['vrf_name'] = want_vrf + undefined_resources.append(undefined) + if check_neighbors and have_conf: + want_neighbors = want_conf.get('neighbors', []) + have_neighbors = have_conf.get('neighbors', []) + undefined_neighbors = get_undefined_neighbors(want_neighbors, have_neighbors) + if undefined_neighbors: + undefined['bgp_as'] = want_bgp_as + undefined['vrf_name'] = want_vrf + undefined['neighbors'] = undefined_neighbors + undefined_resources.append(undefined) + + return undefined_resources + + +def get_undefined_neighbors(want, have): + undefined_neighbors = [] + if not want: + return undefined_neighbors + + if not have: + have = [] + + for want_neighbor in want: + want_neighbor_val = want_neighbor['neighbor'] + have_neighbor = next((conf for conf in have if want_neighbor_val == conf['neighbor']), None) + if not have_neighbor: + undefined_neighbors.append({'neighbor': want_neighbor_val}) + + return undefined_neighbors + + +def validate_bgps(module, want, have): + validate_bgp_resources(module, want, have) + + +def validate_bgp_neighbors(module, want, have): + validate_bgp_resources(module, want, have, check_neighbors=True) + + +def validate_bgp_resources(module, want, have, check_neighbors=None): + undefined_resources = get_undefined_bgps(want, have, check_neighbors) + if undefined_resources: + err = "Resource not found! {res}".format(res=undefined_resources) + module.fail_json(msg=err, code=404) + + +def normalize_neighbors_interface_name(want, module): + if want: + for conf in want: + neighbors = conf.get('neighbors', None) + if neighbors: + normalize_interface_name(neighbors, module, 'neighbor') diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/interfaces_util.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/interfaces_util.py new file mode 100644 index 000000000..a7f6e9063 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/interfaces_util.py @@ -0,0 +1,55 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 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. +# +############################################# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import traceback +import json + +from ansible.module_utils._text import to_native + +try: + import jinja2 + HAS_LIB = True +except Exception as e: + HAS_LIB = False + ERR_MSG = to_native(e) + LIB_IMP_ERR = traceback.format_exc() + + +# To create Loopback, VLAN interfaces +def build_interfaces_create_request(interface_name): + url = "data/openconfig-interfaces:interfaces" + method = "PATCH" + payload_template = """{"openconfig-interfaces:interfaces": {"interface": [{"name": "{{interface_name}}", "config": {"name": "{{interface_name}}"}}]}}""" + input_data = {"interface_name": interface_name} + env = jinja2.Environment(autoescape=False) + t = env.from_string(payload_template) + intended_payload = t.render(input_data) + ret_payload = json.loads(intended_payload) + request = {"path": url, + "method": method, + "data": ret_payload} + return request diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/utils.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/utils.py new file mode 100644 index 000000000..0d6e6d1a0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/utils.py @@ -0,0 +1,511 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2020 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) + +# utils + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import re +import json +import ast +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + is_masklen, + to_netmask, + remove_empties +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + +DEFAULT_TEST_KEY = {'config': {'name': ''}} +GET = 'get' + +intf_naming_mode = "" + + +def get_diff(base_data, compare_with_data, test_keys=None, is_skeleton=None): + diff = [] + if is_skeleton is None: + is_skeleton = False + + test_keys = normalize_testkeys(test_keys) + + if isinstance(base_data, list) and isinstance(compare_with_data, list): + dict_diff = get_diff_dict({"config": base_data}, {"config": compare_with_data}, test_keys, is_skeleton) + diff = dict_diff.get("config", []) + + else: + new_base, new_compare = convert_dict_to_single_entry_list(base_data, compare_with_data, test_keys) + diff = get_diff_dict(new_base, new_compare, test_keys, is_skeleton) + if diff: + diff = convert_single_entry_list_to_dict(diff) + else: + diff = {} + + return diff + + +def get_diff_dict(base_data, compare_with_data, test_keys=None, is_skeleton=None): + if is_skeleton is None: + is_skeleton = False + + if test_keys is None: + test_keys = [] + + if not base_data: + return base_data + + planned_set = set(base_data.keys()) + discovered_set = set(compare_with_data.keys()) + intersect_set = planned_set.intersection(discovered_set) + changed_dict = {} + has_dict_item = None + added_set = planned_set - intersect_set + # Keys part of added are new and put into changed_dict + if added_set: + for key in added_set: + if is_skeleton: + changed_dict[key] = base_data[key] + elif base_data[key] is not None: + if isinstance(base_data[key], dict): + val_dict = remove_empties(base_data[key]) + if val_dict: + changed_dict[key] = remove_empties(base_data[key]) + elif isinstance(base_data[key], list): + val_list = remove_empties_from_list(base_data[key]) + if val_list: + changed_dict[key] = remove_empties_from_list(base_data[key]) + else: + changed_dict[key] = base_data[key] + for key in intersect_set: + has_dict_item = False + value = base_data[key] + if isinstance(value, list): + p_list = base_data[key] if key in base_data else [] + d_list = compare_with_data[key] if key in compare_with_data else [] + keys_to_compare = next((test_key_item[key] for test_key_item in test_keys if key in test_key_item), None) + changed_list = [] + if p_list and d_list: + for p_list_item in p_list: + matched = False + has_diff = False + for d_list_item in d_list: + if (isinstance(p_list_item, dict) and isinstance(d_list_item, dict)): + if keys_to_compare: + key_matched_cnt = 0 + test_keys_present_cnt = 0 + common_keys = set(p_list_item).intersection(d_list_item) + for test_key in keys_to_compare: + if test_key in common_keys: + test_keys_present_cnt += 1 + if p_list_item[test_key] == d_list_item[test_key]: + key_matched_cnt += 1 + if key_matched_cnt and key_matched_cnt == test_keys_present_cnt: + remaining_keys = [test_key_item for test_key_item in test_keys if key not in test_key_item] + dict_diff = get_diff_dict(p_list_item, d_list_item, remaining_keys, is_skeleton) + matched = True + if dict_diff: + has_diff = True + for test_key in keys_to_compare: + dict_diff.update({test_key: p_list_item[test_key]}) + break + else: + dict_diff = get_diff_dict(p_list_item, d_list_item, test_keys, is_skeleton) + if not dict_diff: + matched = True + break + else: + if p_list_item == d_list_item: + matched = True + break + if not matched: + if is_skeleton: + changed_list.append(p_list_item) + else: + if isinstance(p_list_item, dict): + val_dict = remove_empties(p_list_item) + if val_dict is not None: + changed_list.append(val_dict) + elif isinstance(p_list_item, list): + val_list = remove_empties_from_list(p_list_item) + if val_list is not None: + changed_list.append(val_list) + else: + if p_list_item is not None: + changed_list.append(p_list_item) + elif has_diff and dict_diff: + changed_list.append(dict_diff) + if changed_list: + changed_dict.update({key: changed_list}) + elif p_list and (not d_list): + changed_dict[key] = p_list + elif (isinstance(value, dict) and isinstance(compare_with_data[key], dict)): + dict_diff = get_diff_dict(base_data[key], compare_with_data[key], test_keys, is_skeleton) + if dict_diff: + changed_dict[key] = dict_diff + elif value is not None: + if not is_skeleton: + if compare_with_data[key] != base_data[key]: + changed_dict[key] = base_data[key] + return changed_dict + + +def convert_dict_to_single_entry_list(base_data, compare_with_data, test_keys): + # if it is dict comparision convert dict into single entry list by adding 'config' as key + new_base = {'config': [base_data]} + new_compare = {'config': [compare_with_data]} + + # get testkey of 'config' + config_testkey = None + for item in test_keys: + for key, val in item.items(): + if key == 'config': + config_testkey = list(val)[0] + break + if config_testkey: + break + # if testkey of 'config' is not in base data, introduce single entry list + # with 'temp_key' as config testkey and base_data as data. + if config_testkey and base_data and config_testkey not in base_data: + new_base = {'config': [{config_testkey: 'temp_key', 'data': base_data}]} + new_compare = {'config': [{config_testkey: 'temp_key', 'data': compare_with_data}]} + + return new_base, new_compare + + +def convert_single_entry_list_to_dict(diff): + diff = diff['config'][0] + if 'data' in diff: + diff = diff['data'] + return diff + + +def normalize_testkeys(test_keys): + if test_keys is None: + test_keys = [] + + if not any(test_key_item for test_key_item in test_keys if "config" in test_key_item): + test_keys.append(DEFAULT_TEST_KEY) + + return test_keys + + +def update_states(commands, state): + ret_list = list() + if commands: + if isinstance(commands, list): + for command in commands: + ret = command.copy() + ret.update({"state": state}) + ret_list.append(ret) + elif isinstance(commands, dict): + ret_list.append(commands.copy()) + ret_list[0].update({"state": state}) + return ret_list + + +def dict_to_set(sample_dict): + # Generate a set with passed dictionary for comparison + test_dict = dict() + if isinstance(sample_dict, dict): + for k, v in iteritems(sample_dict): + if v is not None: + if isinstance(v, list): + if isinstance(v[0], dict): + li = [] + for each in v: + for key, value in iteritems(each): + if isinstance(value, list): + each[key] = tuple(value) + li.append(tuple(iteritems(each))) + v = tuple(li) + else: + v = tuple(v) + elif isinstance(v, dict): + li = [] + for key, value in iteritems(v): + if isinstance(value, list): + v[key] = tuple(value) + li.extend(tuple(iteritems(v))) + v = tuple(li) + test_dict.update({k: v}) + return_set = set(tuple(iteritems(test_dict))) + else: + return_set = set(sample_dict) + return return_set + + +def validate_ipv4(value, module): + if value: + address = value.split("/") + if len(address) != 2: + module.fail_json( + msg="address format is <ipv4 address>/<mask>, got invalid format {0}".format( + value + ) + ) + + if not is_masklen(address[1]): + module.fail_json( + msg="invalid value for mask: {0}, mask should be in range 0-32".format( + address[1] + ) + ) + + +def validate_ipv6(value, module): + if value: + address = value.split("/") + if len(address) != 2: + module.fail_json( + msg="address format is <ipv6 address>/<mask>, got invalid format {0}".format( + value + ) + ) + else: + if not 0 <= int(address[1]) <= 128: + module.fail_json( + msg="invalid value for mask: {0}, mask should be in range 0-128".format( + address[1] + ) + ) + + +def validate_n_expand_ipv4(module, want): + # Check if input IPV4 is valid IP and expand IPV4 with its subnet mask + ip_addr_want = want.get("address") + if len(ip_addr_want.split(" ")) > 1: + return ip_addr_want + validate_ipv4(ip_addr_want, module) + ip = ip_addr_want.split("/") + if len(ip) == 2: + ip_addr_want = "{0} {1}".format(ip[0], to_netmask(ip[1])) + + return ip_addr_want + + +def netmask_to_cidr(netmask): + bit_range = [128, 64, 32, 16, 8, 4, 2, 1] + count = 0 + cidr = 0 + netmask_list = netmask.split(".") + netmask_calc = [i for i in netmask_list if int(i) != 255 and int(i) != 0] + if netmask_calc: + netmask_calc_index = netmask_list.index(netmask_calc[0]) + elif sum(list(map(int, netmask_list))) == 0: + return "32" + else: + return "24" + for each in bit_range: + if cidr == int(netmask.split(".")[2]): + if netmask_calc_index == 1: + return str(8 + count) + elif netmask_calc_index == 2: + return str(8 * 2 + count) + elif netmask_calc_index == 3: + return str(8 * 3 + count) + break + cidr += each + count += 1 + + +def remove_empties_from_list(config_list): + ret_config = [] + if not config_list: + return ret_config + for config in config_list: + ret_config.append(remove_empties(config)) + return ret_config + + +def get_device_interface_naming_mode(module): + intf_naming_mode = "" + request = {"path": "data/sonic-device-metadata:sonic-device-metadata/DEVICE_METADATA/DEVICE_METADATA_LIST=localhost", "method": GET} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + if 'sonic-device-metadata:DEVICE_METADATA_LIST' in response[0][1]: + device_meta_data = response[0][1].get('sonic-device-metadata:DEVICE_METADATA_LIST', []) + if device_meta_data: + intf_naming_mode = device_meta_data[0].get('intf_naming_mode', 'native') + + return intf_naming_mode + + +STANDARD_ETH_REGEXP = r"[e|E]th\s*\d+/\d+" +NATIVE_ETH_REGEXP = r"[e|E]th*\d+$" +NATIVE_MODE = "native" +STANDARD_MODE = "standard" + + +def find_intf_naming_mode(intf_name): + ret_intf_naming_mode = NATIVE_MODE + + if re.search(STANDARD_ETH_REGEXP, intf_name): + ret_intf_naming_mode = STANDARD_MODE + + return ret_intf_naming_mode + + +def validate_intf_naming_mode(intf_name, module): + global intf_naming_mode + if intf_naming_mode == "": + intf_naming_mode = get_device_interface_naming_mode(module) + + if intf_naming_mode != "": + ansible_intf_naming_mode = find_intf_naming_mode(intf_name) + if intf_naming_mode != ansible_intf_naming_mode: + err = "Interface naming mode configured on switch {naming_mode}, {intf_name} is not valid".format(naming_mode=intf_naming_mode, intf_name=intf_name) + module.fail_json(msg=err, code=400) + + +def normalize_interface_name(configs, module, namekey=None): + if not namekey: + namekey = 'name' + + if configs: + for conf in configs: + if conf.get(namekey, None): + conf[namekey] = get_normalize_interface_name(conf[namekey], module) + + +def normalize_interface_name_list(configs, module): + norm_configs = [] + if configs: + for conf in configs: + conf = get_normalize_interface_name(conf, module) + norm_configs.append(conf) + + return norm_configs + + +def get_normalize_interface_name(intf_name, module): + change_flag = False + # remove the space in the given string + ret_intf_name = re.sub(r"\s+", "", intf_name, flags=re.UNICODE) + ret_intf_name = ret_intf_name.capitalize() + + # serach the numeric charecter(digit) + match = re.search(r"\d", ret_intf_name) + if match: + change_flag = True + start_pos = match.start() + name = ret_intf_name[0:start_pos] + intf_id = ret_intf_name[start_pos:] + + # Interface naming mode affects only ethernet ports + if name.startswith("Eth"): + validate_intf_naming_mode(intf_name, module) + + if ret_intf_name.startswith("Management") or ret_intf_name.startswith("Mgmt"): + name = "eth" + intf_id = "0" + elif re.search(STANDARD_ETH_REGEXP, ret_intf_name): + name = "Eth" + elif re.search(NATIVE_ETH_REGEXP, ret_intf_name): + name = "Ethernet" + elif name.startswith("Po"): + name = "PortChannel" + elif name.startswith("Vlan"): + name = "Vlan" + elif name.startswith("Lo"): + name = "Loopback" + else: + change_flag = False + + ret_intf_name = name + intf_id + + if not change_flag: + ret_intf_name = intf_name + + return ret_intf_name + + +def get_speed_from_breakout_mode(breakout_mode): + speed = None + speed_breakout_mode_map = { + "4x10G": "SPEED_10GB", "1x100G": "SPEED_100GB", "1x40G": "SPEED_40GB", "4x25G": "SPEED_25GB", "2x50G": "SPEED_50GB", + "1x400G": "SPEED_400GB", "4x100G": "SPEED_100GB", "4x50G": "SPEED_50GB", "2x100G": "SPEED_100GB", "2x200G": "SPEED_200GB" + } + if breakout_mode in speed_breakout_mode_map: + speed = speed_breakout_mode_map[breakout_mode] + return speed + + +def get_breakout_mode(module, name): + response = None + mode = None + component_name = name + if "/" in name: + component_name = name.replace("/", "%2f") + url = "data/openconfig-platform:components/component=%s" % (component_name) + request = [{"path": url, "method": GET}] + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + try: + json_obj = json.loads(str(exc).replace("'", '"')) + if json_obj and type(json_obj) is dict and 404 == json_obj['code']: + response = None + else: + module.fail_json(msg=str(exc), code=exc.code) + except Exception as err: + module.fail_json(msg=str(exc), code=exc.code) + + if response and "openconfig-platform:component" in response[0][1]: + raw_port_breakout = response[0][1]['openconfig-platform:component'][0] + port_name = raw_port_breakout.get('name', None) + port_data = raw_port_breakout.get('port', None) + if port_name and port_data and 'openconfig-platform-port:breakout-mode' in port_data: + if 'groups' in port_data['openconfig-platform-port:breakout-mode']: + group = port_data['openconfig-platform-port:breakout-mode']['groups']['group'][0] + if 'config' in group: + cfg = group.get('config', None) + breakout_speed = cfg.get('breakout-speed', None) + num_breakouts = cfg.get('num-breakouts', None) + if breakout_speed and num_breakouts: + speed = breakout_speed.replace('openconfig-if-ethernet:SPEED_', '') + speed = speed.replace('GB', 'G') + mode = str(num_breakouts) + 'x' + speed + return mode + + +def command_list_str_to_dict(module, warnings, cmd_list_in, exec_cmd=False): + cmd_list_out = [] + for cmd in cmd_list_in: + cmd_out = dict() + nested_cmd_is_dict = False + if isinstance(cmd, dict): + cmd_out = cmd + else: + try: + nest_dict = ast.literal_eval(cmd) + nested_cmd_is_dict = isinstance(nest_dict, dict) + except Exception: + nested_cmd_is_dict = False + + if nested_cmd_is_dict: + for key, value in nest_dict.items(): + cmd_out[key] = value + else: + cmd_out = cmd + + if exec_cmd and module.check_mode and not cmd_out['command'].startswith('show'): + warnings.append( + 'Only show commands are supported when using check mode, not ' + 'executing %s' % cmd_out['command'] + ) + else: + cmd_list_out.append(cmd_out) + + return cmd_list_out diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_aaa.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_aaa.py new file mode 100644 index 000000000..ddc71331f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_aaa.py @@ -0,0 +1,215 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 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_aaa +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_aaa +version_added: 1.1.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Abirami N (@abirami-n) +short_description: Manage AAA and its parameters +description: + - This module is used for configuration management of aaa parameters on devices running Enterprise SONiC. +options: + config: + description: + - Specifies the aaa related configurations + type: dict + suboptions: + authentication: + description: + - Specifies the configurations required for aaa authentication + type: dict + suboptions: + data: + description: + - Specifies the data required for aaa authentication + type: dict + suboptions: + fail_through: + description: + - Specifies the state of failthrough + type: bool + local: + description: + - Enable or Disable local authentication + type: bool + group: + description: + - Specifies the method of aaa authentication + type: str + choices: + - ldap + - radius + - tacacs+ + + state: + description: + - Specifies the operation to be performed on the aaa parameters configured on the device. + - In case of merged, the input configuration will be merged with the existing aaa configuration on the device. + - In case of deleted the existing aaa configuration will be removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : True +# login-method : local + +- name: Delete aaa configurations + dellemc.enterprise_sonic.sonic_aaa: + config: + authentication: + data: + local: True + state: deleted + +# After state: +# ------------ +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : True +# login-method : + + +# Using deleted +# +# Before state: +# ------------- +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : True +# login-method : local + +- name: Delete aaa configurations + dellemc.enterprise_sonic.sonic_aaa: + config: + state: deleted + +# After state: +# ------------ +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : +# login-method : + + +# Using merged +# +# Before state: +# ------------- +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : False +# login-method : + +- name: Merge aaa configurations + dellemc.enterprise_sonic.sonic_aaa: + config: + authentication: + data: + local: true + fail_through: true + state: merged + +# After state: +# ------------ +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : True +# login-method : local + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.aaa.aaa import AaaArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.aaa.aaa import Aaa + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=AaaArgs.argument_spec, + supports_check_mode=True) + + result = Aaa(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_api.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_api.py new file mode 100644 index 000000000..234603a0a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_api.py @@ -0,0 +1,158 @@ +#!/usr/bin/python +# +# (c) 2015 Peter Sprygada, <psprygada@ansible.com> +# Copyright (c) 2020 Dell Inc. +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for sonic_vlans +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_api +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Abirami N (@abirami-n) +short_description: Manages REST operations on devices running Enterprise SONiC +description: + - Manages REST operations on devices running Enterprise SONiC Distribution + by Dell Technologies. This module provides an implementation for working + with SONiC REST operations in a deterministic way. +options: + url: + description: + - The HTTP path of the request after 'restconf/'. + type: path + required: true + body: + description: + - The body of the HTTP request/response to the web service which contains the payload. + type: raw + method: + description: + - The HTTP method of the request or response. Must be a valid method + accepted by the service that handles the request. + type: str + required: true + choices: ['GET', 'PUT', 'POST', 'PATCH', 'DELETE'] + status_code: + description: + - A list of valid, numeric, HTTP status codes that signifies the success of a request. + type: list + elements: int + required: true +""" +EXAMPLES = """ +- name: Checks that you can connect (GET) to a page and it returns a status 200 + dellemc.enterprise_sonic.sonic_api: + url: data/openconfig-interfaces:interfaces/interface=Ethernet60 + method: "GET" + status_code: 200 + +- name: Appends data to an existing interface using PATCH and verifies if it returns status 204 + dellemc.enterprise_sonic.sonic_api: + url: data/openconfig-interfaces:interfaces/interface=Ethernet60/config/description + method: "PATCH" + body: {"openconfig-interfaces:description": "Eth-60"} + status_code: 204 + +- name: Deletes an associated IP address using DELETE and verifies if it returns status 204 + dellemc.enterprise_sonic.sonic_api: + url: > + data/openconfig-interfaces:interfaces/interface=Ethernet64/subinterfaces/subinterface=0/ + openconfig-if-ip:ipv4/addresses/address=1.1.1.1/config/prefix-length + method: "DELETE" + status_code: 204 + +- name: Adds a VLAN network instance using PUT and verifies if it returns status 204 + dellemc.enterprise_sonic.sonic_api: + url: data/openconfig-network-instance:network-instances/network-instance=Vlan100/ + method: "PUT" + body: {"openconfig-network-instance:network-instance": [{"name": "Vlan100","config": {"name": "Vlan100"}}]} + status_code: 204 + +- name: Adds a prefix-set to a routing policy using POST and verifies if it returns 201 + dellemc.enterprise_sonic.sonic_api: + url: data/openconfig-routing-policy:routing-policy/defined-sets/prefix-sets/prefix-set=p1 + method: "POST" + body: {"openconfig-routing-policy:config": {"name": "p1","mode": "IPV4" }} + status_code: 201 + +""" +RETURN = """ +response: + description: The response at the network device end for the REST call which contains the status code. + returned: always + type: list + sample: {"response": [ 204,{""}]} +msg: + description: The HTTP error message from the request. + returned: HTTP Error + type: str +""" + +from ansible.module_utils.connection import ConnectionError + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import edit_config, to_request + + +def initiate_request(module): + """Get all the data available in chassis""" + url = module.params['url'] + body = module.params['body'] + method = module.params['method'] + if method == "GET" or method == "DELETE": + request = to_request(module, [{"path": url, "method": method}]) + elif method == "PATCH" or method == "PUT" or method == "POST": + request = to_request(module, [{"path": url, "method": method, "data": body}]) + + try: + response = edit_config(module, request) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) + return response + + +def main(): + + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + argument_spec = dict( + url=dict(type='path', required=True), + body=dict(type='raw', required=False), + method=dict(type='str', choices=['GET', 'PUT', 'PATCH', 'DELETE', 'POST'], required=True), + status_code=dict(type='list', elements='int', required=True), + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + result = dict( + changed=False, + ) + response = initiate_request(module) + response_code = response[0][0] + status_code = module.params['status_code'] + if response_code == int(status_code[0]) and response_code in (201, 204): + result.update({'changed': True}) + + result.update({ + 'response': response, + }) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp.py new file mode 100644 index 000000000..bc53ca40c --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp.py @@ -0,0 +1,390 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2020 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_bgp +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_bgp +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Dhivya P (@dhivayp) +short_description: Manage global BGP and its parameters +description: + - This module provides configuration management of global BGP parameters on devices running Enterprise SONiC Distribution by Dell Technologies. +options: + config: + description: + - Specifies the BGP-related configuration. + type: list + elements: dict + suboptions: + bgp_as: + description: + - Specifies the BGP autonomous system (AS) number to configure on the device. + type: str + required: true + vrf_name: + description: + - Specifies the VRF name. + type: str + default: 'default' + router_id: + description: + - Configures the BGP routing process router-id value. + type: str + log_neighbor_changes: + description: + - Enables/disables logging neighbor up/down and reset reason. + type: bool + max_med: + description: + - Configure max med and its parameters + type: dict + suboptions: + on_startup: + description: + - On startup time and max-med value + type: dict + suboptions: + timer: + description: + - Configures on startup time + type: int + med_val: + description: + - on startup med value + type: int + timers: + description: + - Adjust routing timers + type: dict + suboptions: + holdtime: + description: + - Configures hold-time + type: int + keepalive_interval: + description: + - Configures keepalive-interval + type: int + bestpath: + description: + - Configures the BGP best-path. + type: dict + suboptions: + as_path: + description: + - Configures the as-path values. + type: dict + suboptions: + confed: + description: + - Configures the confed values of as-path. + type: bool + ignore: + description: + - Configures the ignore values of as-path. + type: bool + multipath_relax: + description: + - Configures the multipath_relax values of as-path. + type: bool + multipath_relax_as_set: + description: + - Configures the multipath_relax_as_set values of as-path. + type: bool + compare_routerid: + description: + - Configures the compare_routerid. + type: bool + med: + description: + - Configures the med values. + type: dict + suboptions: + confed: + description: + - Configures the confed values of med. + type: bool + missing_as_worst: + description: + - Configures the missing_as_worst values of as-path. + type: bool + always_compare_med: + description: + - Allows comparing meds from different neighbors if set to true + type: bool + state: + description: + - Specifies the operation to be performed on the BGP process that is configured on the device. + - In case of merged, the input configuration is merged with the existing BGP configuration on the device. + - In case of deleted, the existing BGP configuration is removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#! +#router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# log-neighbor-changes +#! +#router bgp 11 vrf VrfCheck2 +# log-neighbor-changes +# bestpath as-path ignore +# bestpath med missing-as-worst confed +# bestpath compare-routerid +#! +#router bgp 4 +# router-id 10.2.2.4 +# bestpath as-path ignore +# bestpath as-path confed +# bestpath med missing-as-worst confed +# bestpath compare-routerid +#! +# +- name: Delete BGP Global attributes + dellemc.enterprise_sonic.sonic_bgp: + config: + - bgp_as: 4 + router_id: 10.2.2.4 + log_neighbor_changes: False + bestpath: + as_path: + confed: True + ignore: True + multipath_relax: False + multipath_relax_as_set: True + compare_routerid: True + med: + confed: True + missing_as_worst: True + - bgp_as: 10 + router_id: 10.2.2.32 + log_neighbor_changes: True + vrf_name: 'VrfCheck1' + - bgp_as: 11 + log_neighbor_changes: True + vrf_name: 'VrfCheck2' + bestpath: + as_path: + confed: False + ignore: True + multipath_relax_as_set: True + compare_routerid: True + med: + confed: True + missing_as_worst: True + state: deleted + + +# After state: +# ------------ +# +#! +#router bgp 10 vrf VrfCheck1 +# log-neighbor-changes +#! +#router bgp 11 vrf VrfCheck2 +# log-neighbor-changes +# bestpath compare-routerid +#! +#router bgp 4 +# log-neighbor-changes +# bestpath compare-routerid +#! + + +# Using deleted +# +# Before state: +# ------------- +# +#! +#router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# log-neighbor-changes +#! +#router bgp 11 vrf VrfCheck2 +# log-neighbor-changes +# bestpath as-path ignore +# bestpath med missing-as-worst confed +# bestpath compare-routerid +#! +#router bgp 4 +# router-id 10.2.2.4 +# bestpath as-path ignore +# bestpath as-path confed +# bestpath med missing-as-worst confed +# bestpath compare-routerid +#! + +- name: Deletes all the bgp global configurations + dellemc.enterprise_sonic.sonic_bgp: + config: + state: deleted + +# After state: +# ------------ +# +#! +#! + + +# Using merged +# +# Before state: +# ------------- +# +#! +#router bgp 4 +# router-id 10.1.1.4 +#! +# +- name: Merges provided configuration with device configuration + dellemc.enterprise_sonic.sonic_bgp: + config: + - bgp_as: 4 + router_id: 10.2.2.4 + log_neighbor_changes: False + timers: + holdtime: 20 + keepalive_interval: 30 + bestpath: + as_path: + confed: True + ignore: True + multipath_relax: False + multipath_relax_as_set: True + compare_routerid: True + med: + confed: True + missing_as_worst: True + always_compare_med: True + max_med: + on_startup: + timer: 667 + med_val: 7878 + - bgp_as: 10 + router_id: 10.2.2.32 + log_neighbor_changes: True + vrf_name: 'VrfCheck1' + - bgp_as: 11 + log_neighbor_changes: True + vrf_name: 'VrfCheck2' + bestpath: + as_path: + confed: False + ignore: True + multipath_relax_as_set: True + compare_routerid: True + med: + confed: True + missing_as_worst: True + state: merged +# +# After state: +# ------------ +# +#! +#router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# log-neighbor-changes +#! +#router bgp 11 vrf VrfCheck2 +# log-neighbor-changes +# bestpath as-path ignore +# bestpath med missing-as-worst confed +# bestpath compare-routerid +#! +#router bgp 4 +# router-id 10.2.2.4 +# bestpath as-path ignore +# bestpath as-path confed +# bestpath med missing-as-worst confed +# bestpath compare-routerid +# always-compare-med +# max-med on-startup 667 7878 +# timers 20 30 +# +#! + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.bgp.bgp import BgpArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bgp.bgp import Bgp + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=BgpArgs.argument_spec, + supports_check_mode=True) + + result = Bgp(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_af.py new file mode 100644 index 000000000..6d55355c9 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_af.py @@ -0,0 +1,414 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2020 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_bgp_af +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_bgp_af +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Niraimadaiselvam M (@niraimadaiselvamm) +short_description: Manage global BGP address-family and its parameters +description: + - This module provides configuration management of global BGP_AF parameters on devices running Enterprise SONiC. + - bgp_as and vrf_name must be created in advance on the device. +options: + config: + description: + - Specifies the BGP_AF related configuration. + type: list + elements: dict + suboptions: + bgp_as: + description: + - Specifies the BGP autonomous system (AS) number which is already configured on the device. + type: str + required: true + vrf_name: + description: + - Specifies the VRF name which is already configured on the device. + type: str + default: 'default' + address_family: + description: + - Specifies BGP address family related configurations. + type: dict + suboptions: + afis: + description: + - List of address families, such as ipv4, ipv6, and l2vpn. + - afi and safi are required together. + type: list + elements: dict + suboptions: + afi: + description: + - Type of address family to configure. + type: str + choices: + - ipv4 + - ipv6 + - l2vpn + required: True + safi: + description: + - Specifies the type of communication for the address family. + type: str + choices: + - unicast + - evpn + default: unicast + dampening: + description: + - Enable route flap dampening if set to true + type: bool + network: + description: + - Enable routing on an IP network for each prefix provided in the network + type: list + elements: str + redistribute: + description: + - Specifies the redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + protocol: + description: + - Specifies the protocol for configuring redistribute information. + type: str + choices: ['ospf', 'static', 'connected'] + required: True + metric: + description: + - Specifies the metric for redistributed routes. + type: str + route_map: + description: + - Specifies the route map reference. + type: str + advertise_pip: + description: + - Enables advertise PIP + type: bool + advertise_pip_ip: + description: + - PIP IPv4 address + type: str + advertise_pip_peer_ip: + description: + - PIP peer IPv4 address + type: str + advertise_svi_ip: + description: + - Enables advertise SVI MACIP routes + type: bool + route_advertise_list: + description: + - List of advertise routes + type: list + elements: dict + suboptions: + advertise_afi: + required: True + type: str + choices: + - ipv4 + - ipv6 + description: + - Specifies the address family + route_map: + type: str + description: + - Specifies the route-map reference + advertise_default_gw: + description: + - Specifies the advertise default gateway flag. + type: bool + advertise_all_vni: + description: + - Specifies the advertise all vni flag. + type: bool + max_path: + description: + - Specifies the maximum paths of ibgp and ebgp count. + type: dict + suboptions: + ibgp: + description: + - Specifies the count of the ibgp multipaths count. + type: int + ebgp: + description: + - Specifies the count of the ebgp multipaths count. + type: int + state: + description: + - Specifies the operation to be performed on the BGP_AF process configured on the device. + - In case of merged, the input configuration is merged with the existing BGP_AF configuration on the device. + - In case of deleted, the existing BGP_AF configuration is removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#do show running-configuration bgp +#! +#router bgp 51 +# router-id 111.2.2.41 +# timers 60 180 +# ! +# address-family ipv4 unicast +# maximum-paths 1 +# maximum-paths ibgp 1 +# dampening +# ! +# address-family ipv6 unicast +# redistribute connected route-map bb metric 21 +# redistribute ospf route-map aa metric 27 +# redistribute static route-map bb metric 26 +# maximum-paths 4 +# maximum-paths ibgp 5 +# ! +# address-family l2vpn evpn +# advertise-svi-ip +# advertise ipv6 unicast route-map aa +# advertise-pip ip 1.1.1.1 peer-ip 2.2.2.2 +#! +# +- name: Delete BGP Address family configuration from the device + dellemc.enterprise_sonic.sonic_bgp_af: + config: + - bgp_as: 51 + address_family: + afis: + - afi: l2vpn + safi: evpn + advertise_pip: True + advertise_pip_ip: "1.1.1.1" + advertise_pip_peer_ip: "2.2.2.2" + advertise_svi_ip: True + advertise_all_vni: False + advertise_default_gw: False + route_advertise_list: + - advertise_afi: ipv6 + route_map: aa + - afi: ipv4 + safi: unicast + - afi: ipv6 + safi: unicast + max_path: + ebgp: 2 + ibgp: 5 + redistribute: + - metric: "21" + protocol: connected + route_map: bb + - metric: "27" + protocol: ospf + route_map: aa + - metric: "26" + protocol: static + route_map: bb + state: deleted + +# After state: +# ------------ +# +#do show running-configuration bgp +#! +#router bgp 51 +# router-id 111.2.2.41 +# timers 60 180 +# ! +# address-family ipv6 unicast +# ! +# address-family l2vpn evpn +# +# Using deleted +# +# Before state: +# ------------- +# +#do show running-configuration bgp +#! +#router bgp 51 +# router-id 111.2.2.41 +# timers 60 180 +# ! +# address-family ipv6 unicast +# ! +# address-family l2vpn evpn +# +- name: Delete All BGP address family configurations + dellemc.enterprise_sonic.sonic_bgp_af: + config: + state: deleted + + +# After state: +# ------------ +# +#do show running-configuration bgp +#! +#router bgp 51 +# router-id 111.2.2.41 +# timers 60 180 +# +# Using merged +# +# Before state: +# ------------- +# +#do show running-configuration bgp +#! +#router bgp 51 +# router-id 111.2.2.41 +# timers 60 180 +# ! +# address-family l2vpn evpn +# +- name: Merge provided BGP address family configuration on the device. + dellemc.enterprise_sonic.sonic_bgp_af: + config: + - bgp_as: 51 + address_family: + afis: + - afi: l2vpn + safi: evpn + advertise_pip: True + advertise_pip_ip: "3.3.3.3" + advertise_pip_peer_ip: "4.4.4.4" + advertise_svi_ip: True + advertise_all_vni: False + advertise_default_gw: False + route_advertise_list: + - advertise_afi: ipv4 + route_map: bb + - afi: ipv4 + safi: unicast + network: + - 2.2.2.2/16 + - 192.168.10.1/32 + dampening: True + - afi: ipv6 + safi: unicast + max_path: + ebgp: 4 + ibgp: 5 + redistribute: + - metric: "21" + protocol: connected + route_map: bb + - metric: "27" + protocol: ospf + route_map: aa + - metric: "26" + protocol: static + route_map: bb + state: merged +# After state: +# ------------ +# +#do show running-configuration bgp +#! +#router bgp 51 +# router-id 111.2.2.41 +# timers 60 180 +# ! +# address-family ipv4 unicast +# network 2.2.2.2/16 +# network 192.168.10.1/32 +# dampening +# ! +# address-family ipv6 unicast +# redistribute connected route-map bb metric 21 +# redistribute ospf route-map aa metric 27 +# redistribute static route-map bb metric 26 +# maximum-paths 4 +# maximum-paths ibgp 5 +# ! +# address-family l2vpn evpn +# advertise-svi-ip +# advertise ipv4 unicast route-map bb +# advertise-pip ip 3.3.3.3 peer-ip 4.4.4.4 +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned always in the same format + of 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.bgp_af.bgp_af import Bgp_afArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bgp_af.bgp_af import Bgp_af + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Bgp_afArgs.argument_spec, + supports_check_mode=True) + + result = Bgp_af(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_as_paths.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_as_paths.py new file mode 100644 index 000000000..bd2ff74a1 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_as_paths.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_as_paths +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_bgp_as_paths +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage BGP autonomous system path (or as-path-list) and its parameters +description: + - This module provides configuration management of BGP bgp_as_paths for devices + running Enterprise SONiC Distribution by Dell Technologies. +author: Kumaraguru Narayanan (@nkumaraguru) +options: + config: + description: A list of 'bgp_as_paths' configurations. + type: list + elements: dict + suboptions: + name: + required: True + type: str + description: + - Name of as-path-list. + members: + required: False + type: list + elements: str + description: + - Members of this BGP as-path; regular expression string can be provided. + permit: + required: False + type: bool + description: + - Permits or denies this as path. + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 808.*,909.* + +- name: Delete BGP as path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + - 909.* + permit: true + state: deleted + +# After state: +# ------------ +# +# show bgp as-path-access-list +# AS path list test: +# action: +# members: 808.* + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 808.*,909.* +# AS path list test1: +# action: deny +# members: 608.*,709.* + +- name: Deletes BGP as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + state: deleted + +# After state: +# ------------ +# +# show bgp as-path-access-list +# AS path list test1: +# action: deny +# members: 608.*,709.* + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 808.*,909.* + +- name: Deletes BGP as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + state: deleted + +# After state: +# ------------ +# +# show bgp as-path-access-list +# (No bgp as-path-access-list configuration present) + + +# Using merged + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: + +- name: Adds 909.* to test as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + - 909.* + permit: true + state: merged + +# After state: +# ------------ +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 909.* + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.bgp_as_paths.bgp_as_paths import Bgp_as_pathsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bgp_as_paths.bgp_as_paths import Bgp_as_paths + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Bgp_as_pathsArgs.argument_spec, + supports_check_mode=True) + + result = Bgp_as_paths(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_communities.py new file mode 100644 index 000000000..08c8dcc7f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_communities.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_communities +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_bgp_communities +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage BGP community and its parameters +description: + - This module provides configuration management of BGP bgp_communities for device + running Enterprise SONiC Distribution by Dell Technologies. +author: Kumaraguru Narayanan (@nkumaraguru) +options: + config: + description: A list of 'bgp_communities' configurations. + type: list + elements: dict + suboptions: + name: + required: True + type: str + description: + - Name of the BGP communitylist. + type: + type: str + description: + - Whether it is a standard or expanded community-list entry. + required: False + choices: + - standard + - expanded + default: standard + permit: + required: False + type: bool + description: + - Permits or denies this community. + aann: + required: False + type: str + description: + - Community number aa:nn format 0..65535:0..65535; applicable for standard BGP community type. + local_as: + required: False + type: bool + description: + - Do not send outside local AS (well-known community); applicable for standard BGP community type. + no_advertise: + required: False + type: bool + description: + - Do not advertise to any peer (well-known community); applicable for standard BGP community type. + no_export: + required: False + type: bool + description: + - Do not export to next AS (well-known community); applicable for standard BGP community type. + no_peer: + required: False + type: bool + description: + - Do not export to next AS (well-known community); applicable for standard BGP community type. + members: + required: False + type: dict + suboptions: + regex: + type: list + elements: str + required: False + description: + - Members of this BGP community list. Regular expression string can be given here. Applicable for expanded BGP community type. + description: + - Members of this BGP community list. + match: + required: False + type: str + description: + - Matches any/all of the members. + choices: + - ALL + - ANY + default: ANY + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted + +# Before state: +# ------------- +# +# show bgp community-list +# Standard community list test: match: ANY +# 101 +# 201 +# Standard community list test1: match: ANY +# 301 + +- name: Deletes BGP community member + dellemc.enterprise_sonic.sonic_bgp_communities: + config: + - name: test + members: + regex: + - 201 + state: deleted + +# After state: +# ------------ +# +# show bgp community-list +# Standard community list test: match: ANY +# 101 +# Standard community list test1: match: ANY +# 301 + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp community-list +# Standard community list test: match: ANY +# 101 +# Expanded community list test1: match: ANY +# 201 + +- name: Deletes a single BGP community + dellemc.enterprise_sonic.sonic_bgp_communities: + config: + - name: test + members: + state: deleted + +# After state: +# ------------ +# +# show bgp community-list +# Expanded community list test1: match: ANY +# 201 + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp community-list +# Standard community list test: match: ANY +# 101 +# Expanded community list test1: match: ANY +# 201 + +- name: Delete All BGP communities + dellemc.enterprise_sonic.sonic_bgp_communities: + config: + state: deleted + +# After state: +# ------------ +# +# show bgp community-list +# + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp community-list +# Standard community list test: match: ANY +# 101 +# Expanded community list test1: match: ANY +# 201 + +- name: Deletes all members in a single BGP community + dellemc.enterprise_sonic.sonic_bgp_communities: + config: + - name: test + members: + regex: + state: deleted + +# After state: +# ------------ +# +# show bgp community-list +# Expanded community list test: match: ANY +# Expanded community list test1: match: ANY +# 201 + + +# Using merged + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: + +- name: Adds 909.* to test as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + - 909.* + state: merged + +# After state: +# ------------ +# +# show bgp as-path-access-list +# AS path list test: +# members: 909.* + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration that is returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration that is returned is always in the same format + of the parameters above. +commands: + description: The set of commands that are 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.bgp_communities.bgp_communities import Bgp_communitiesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bgp_communities.bgp_communities import Bgp_communities + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Bgp_communitiesArgs.argument_spec, + supports_check_mode=True) + + result = Bgp_communities(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_ext_communities.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_ext_communities.py new file mode 100644 index 000000000..c2af0c488 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_ext_communities.py @@ -0,0 +1,288 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_ext_communities +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_bgp_ext_communities +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage BGP extended community-list and its parameters +description: + - This module provides configuration management of BGP extcommunity-list for devices running + Enterprise SONiC Distribution by Dell Technologies. +author: Kumaraguru Narayanan (@nkumaraguru) +options: + config: + description: A list of 'bgp_extcommunity_list' configurations. + type: list + elements: dict + suboptions: + name: + required: True + type: str + description: + - Name of the BGP ext communitylist. + type: + type: str + description: + - Whether it is a standard or expanded ext community_list entry. + required: False + choices: + - standard + - expanded + default: standard + permit: + required: False + type: bool + description: + - Permits or denies this community. + members: + required: False + type: dict + suboptions: + regex: + type: list + elements: str + required: False + description: + - Members of this BGP ext community list. Regular expression string can be given here. Applicable for expanded ext BGP community type. + route_target: + type: list + elements: str + required: False + description: + - Members of this BGP ext community list. The format of route_target is in either 0..65535:0..65535 or A.B.C.D:[1..65535] format. + route_origin: + type: list + elements: str + required: False + description: + - Members of this BGP ext community list. The format of route_origin is in either 0..65535:0..65535 or A.B.C.D:[1..65535] format. + description: + - Members of this BGP ext community list. + match: + required: False + type: str + description: + - Matches any/all of the the members. + choices: + - all + - any + default: any + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted + +# Before state: +# ------------- +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# rt:101:101 +# rt:201:201 + +- name: Deletes a BGP ext community member + dellemc.enterprise_sonic.sonic_bgp_ext_communities: + config: + - name: test + members: + regex: + - 201:201 + state: deleted + +# After state: +# ------------ +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# rt:101:101 +# + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# 101 +# Expanded extended community list test1: match: ANY +# 201 + +- name: Deletes a single BGP extended community + dellemc.enterprise_sonic.sonic_bgp_ext_communities: + config: + - name: test1 + members: + state: deleted + +# After state: +# ------------ +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# 101 +# + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# 101 +# Expanded extended community list test1: match: ANY +# 201 + +- name: Deletes all BGP extended communities + dellemc.enterprise_sonic.sonic_bgp_ext_communities: + config: + state: deleted + +# After state: +# ------------ +# +# show bgp ext-community-list +# + + +# Using deleted + +# Before state: +# ------------- +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# 101 +# Expanded extended community list test1: match: ANY +# 201 + +- name: Deletes all members in a single BGP extended community + dellemc.enterprise_sonic.sonic_bgp_ext_communities: + config: + - name: test1 + members: + regex: + state: deleted + +# After state: +# ------------ +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# 101 +# Expanded extended community list test1: match: ANY +# + + +# Using merged + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: + +- name: Adds 909.* to test as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + - 909.* + state: merged + +# After state: +# ------------ +# +# show bgp as-path-access-list +# AS path list test: +# members: 909.* + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.bgp_ext_communities.bgp_ext_communities import ( + Bgp_ext_communitiesArgs, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bgp_ext_communities.bgp_ext_communities import Bgp_ext_communities + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Bgp_ext_communitiesArgs.argument_spec, + supports_check_mode=True) + + result = Bgp_ext_communities(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors.py new file mode 100644 index 000000000..19aeb6fc9 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors.py @@ -0,0 +1,1112 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_bgp_neighbors +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_bgp_neighbors +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage a BGP neighbor and its parameters +description: + - This module provides configuration management of global BGP_NEIGHBORS parameters on devices running Enterprise SONiC. + - bgp_as and vrf_name must be created on the device in advance. +author: Abirami N (@abirami-n) +options: + config: + description: Specifies the BGP neighbors related configuration. + type: list + elements: dict + suboptions: + bgp_as: + description: + - Specifies the BGP autonomous system (AS) number which is already configured on the device. + type: str + required: True + vrf_name: + description: + - Specifies the VRF name which is already configured on the device. + default: default + type: str + peer_group: + description: Specifies the list of peer groups. + type: list + elements: dict + suboptions: + name: + description: Name of the peer group. + type: str + required: True + remote_as: + description: + - Remote AS of the BGP peer group to configure. + - peer_as and peer_type are mutually exclusive. + type: dict + suboptions: + peer_as: + description: + - Specifies remote AS number. + - The range is from 1 to 4294967295. + type: int + peer_type: + description: + - Specifies the type of BGP peer. + type: str + choices: + - internal + - external + bfd: + description: + - Enables or disables BFD. + type: dict + suboptions: + enabled: + description: + - Enables BFD liveliness check for a BGP peer. + type: bool + check_failure: + description: + - Link dataplane status with control plane. + type: bool + profile: + description: + - BFD Profile name. + type: str + advertisement_interval: + description: + - Specifies the minimum interval between sending BGP routing updates. + - The range is from 0 to 600. + type: int + timers: + description: + - Specifies BGP peer group timer related configurations. + type: dict + suboptions: + keepalive: + description: + - Frequency with which the device sends keepalive messages to its peer, in seconds. + - The range is from 0 to 65535. + type: int + holdtime: + description: + - Interval after not receiving a keepalive message that Enterprise SONiC declares a peer dead, in seconds. + - The range is from 0 to 65535. + type: int + connect_retry: + description: + - Time interval in seconds between attempts to establish a session with the peer. + - The range is from 1 to 65535. + type: int + capability: + description: + - Specifies capability attributes to this peer group. + type: dict + suboptions: + dynamic: + description: + - Enables or disables dynamic capability to this peer group. + type: bool + extended_nexthop: + description: + - Enables or disables advertise extended next-hop capability to the peer. + type: bool + auth_pwd: + description: + - Configuration for peer group authentication password. + type: dict + suboptions: + pwd: + description: + - Authentication password for the peer group. + type: str + required: True + encrypted: + description: + - Indicates whether the password is encrypted text. + type: bool + default: False + pg_description: + description: + - A textual description of the peer group. + type: str + disable_connected_check: + description: + - Disables EBGP conntected route check. + type: bool + dont_negotiate_capability: + description: + - Disables capability negotiation. + type: bool + ebgp_multihop: + description: + - Allow EBGP peers not on directly connected networks. + type: dict + suboptions: + enabled: + description: + - Enables the referenced group or peers to be indirectly connected. + type: bool + default: False + multihop_ttl: + description: + - Time-to-live value to use when packets are sent to the referenced group or peers and ebgp-multihop is enabled. + type: int + enforce_first_as: + description: + - Enforces the first AS for EBGP routes. + type: bool + enforce_multihop: + description: + - Enforces EBGP multihop performance for peer. + type: bool + local_address: + description: + - Set the local IP address to use for the session when sending BGP update messages. + type: str + local_as: + description: + - Specifies local autonomous system number. + type: dict + suboptions: + as: + description: + - Local autonomous system number. + type: int + required: True + no_prepend: + description: + - Do not prepend the local-as number in AS-Path advertisements. + type: bool + replace_as: + description: + - Replace the configured AS Number with the local-as number in AS-Path advertisements. + type: bool + override_capability: + description: + - Override capability negotiation result. + type: bool + passive: + description: + - Do not send open messages to this peer. + type: bool + default: False + shutdown_msg: + description: + - Add a shutdown message. + type: str + solo: + description: + - Indicates that routes advertised by the peer should not be reflected back to the peer. + type: bool + strict_capability_match: + description: + - Enables strict capability negotiation match. + type: bool + ttl_security: + description: + - Enforces only the peers that are specified number of hops away will be allowed to become peers. + type: int + address_family: + description: + - Holds of list of address families associated to the peergroup. + type: dict + suboptions: + afis: + description: + - List of address families with afi, safi, activate and allowas-in parameters. + - afi and safi are required together. + type: list + elements: dict + suboptions: + afi: + description: + - Holds afi mode. + type: str + choices: + - ipv4 + - ipv6 + - l2vpn + safi: + description: + - Holds safi mode. + type: str + choices: + - unicast + - evpn + activate: + description: + - Enable or disable activate. + type: bool + allowas_in: + description: + - Holds AS value. + - The origin and value are mutually exclusive. + type: dict + suboptions: + origin: + description: + - Set AS as the origin. + type: bool + value: + description: + - Holds AS number in the range 1-10. + type: int + ip_afi: + description: + - Common configuration attributes for IPv4 and IPv6 unicast address families. + type: dict + suboptions: + default_policy_name: + description: + - Specifies routing policy definition. + type: str + send_default_route: + description: + - Enable or disable sending of default-route to the peer. + type: bool + default: False + prefix_limit: + description: + - Specifies prefix limit attributes. + type: dict + suboptions: + max_prefixes: + description: + - Maximum number of prefixes that will be accepted from the peer. + type: int + prevent_teardown: + description: + - Enable or disable teardown of BGP session when maximum prefix limit is exceeded. + type: bool + default: False + warning_threshold: + description: + - Threshold on number of prefixes that can be received from a peer before generation of warning messages. + - Expressed as a percentage of max-prefixes. + type: int + restart_timer: + description: + - Time interval in seconds after which the BGP session is re-established after being torn down. + type: int + prefix_list_in: + description: + - Inbound route filtering policy for a peer. + type: str + prefix_list_out: + description: + - Outbound route filtering policy for a peer. + type: str + neighbors: + description: Specifies BGP neighbor-related configurations. + type: list + elements: dict + suboptions: + neighbor: + description: + - Neighbor router address. + type: str + required: True + remote_as: + description: + - Remote AS of the BGP neighbor to configure. + - peer_as and peer_type are mutually exclusive. + type: dict + suboptions: + peer_as: + description: + - Specifies remote AS number. + - The range is from 1 to 4294967295. + type: int + peer_type: + description: + - Specifies the type of BGP peer. + type: str + choices: + - internal + - external + bfd: + description: + - Enables or disables BFD. + type: dict + suboptions: + enabled: + description: + - Enables BFD liveliness check for a BGP neighbor. + type: bool + check_failure: + description: + - Link dataplane status with control plane. + type: bool + profile: + description: + - BFD Profile name. + type: str + advertisement_interval: + description: + - Specifies the minimum interval between sending BGP routing updates. + - The range is from 0 to 600. + type: int + peer_group: + description: + - The name of the peer group that the neighbor is a member of. + type: str + timers: + description: + - Specifies BGP neighbor timer-related configurations. + type: dict + suboptions: + keepalive: + description: + - Frequency with which the device sends keepalive messages to its peer, in seconds. + - The range is from 0 to 65535. + type: int + holdtime: + description: + - Interval after not receiving a keepalive message that SONiC declares a peer dead, in seconds. + - The range is from 0 to 65535. + type: int + connect_retry: + description: + - Time interval in seconds between attempts to establish a session with the peer. + - The range is from 1 to 65535. + type: int + capability: + description: + - Specifies capability attributes to this neighbor. + type: dict + suboptions: + dynamic: + description: + - Enables or disables dynamic capability to this neighbor. + type: bool + extended_nexthop: + description: + - Enables or disables advertise extended next-hop capability to the peer. + type: bool + auth_pwd: + description: + - Configuration for neighbor group authentication password. + type: dict + suboptions: + pwd: + description: + - Authentication password for the neighbor group. + type: str + required: True + encrypted: + description: + - Indicates whether the password is encrypted text. + type: bool + default: False + nbr_description: + description: + - A textual description of the neighbor. + type: str + disable_connected_check: + description: + - Disables EBGP conntected route check. + type: bool + dont_negotiate_capability: + description: + - Disables capability negotiation. + type: bool + ebgp_multihop: + description: + - Allow EBGP neighbors not on directly connected networks. + type: dict + suboptions: + enabled: + description: + - Enables the referenced group or neighbors to be indirectly connected. + type: bool + default: False + multihop_ttl: + description: + - Time-to-live value to use when packets are sent to the referenced group or neighbors and ebgp-multihop is enabled. + type: int + enforce_first_as: + description: + - Enforces the first AS for EBGP routes. + type: bool + enforce_multihop: + description: + - Enforces EBGP multihop performance for neighbor. + type: bool + local_address: + description: + - Set the local IP address to use for the session when sending BGP update messages. + type: str + local_as: + description: + - Specifies local autonomous system number. + type: dict + suboptions: + as: + description: + - Local autonomous system number. + type: int + required: True + no_prepend: + description: + - Do not prepend the local-as number in AS-Path advertisements. + type: bool + replace_as: + description: + - Replace the configured AS Number with the local-as number in AS-Path advertisements. + type: bool + override_capability: + description: + - Override capability negotiation result. + type: bool + passive: + description: + - Do not send open messages to this neighbor. + type: bool + default: False + port: + description: + - Neighbor's BGP port. + type: int + shutdown_msg: + description: + - Add a shutdown message. + type: str + solo: + description: + - Indicates that routes advertised by the peer should not be reflected back to the peer. + type: bool + strict_capability_match: + description: + - Enables strict capability negotiation match. + type: bool + ttl_security: + description: + - Enforces only the neighbors that are specified number of hops away will be allowed to become neighbors. + type: int + v6only: + description: + - Enables BGP with v6 link-local only. + type: bool + + state: + description: + - Specifies the operation to be performed on the BGP process that is configured on the device. + - In case of merged, the input configuration is merged with the existing BGP configuration on the device. + - In case of deleted, the existing BGP configuration is removed from the device. + default: merged + type: str + choices: + - merged + - deleted +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +#router bgp 11 vrf VrfCheck2 +# network import-check +# timers 60 180 +#! +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +# ! +# neighbor interface Eth1/3 +#! +#router bgp 11 +# network import-check +# timers 60 180 +# ! +# neighbor 192.168.1.4 +# ! +# peer-group SP1 +# bfd +# capability dynamic +# ! +# peer-group SP2 +# ! +# +- name: Deletes all BGP neighbors + dellemc.enterprise_sonic.sonic_bgp_neighbors: + config: + state: deleted + +# +# After state: +# ------------- +#router bgp 11 vrf VrfCheck2 +# network import-check +# timers 60 180 +#! +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +#! +#router bgp 11 +# network import-check +# timers 60 180 +# ! +# +# Using merged +# +# Before state: +# ------------ +#router bgp 11 vrf VrfCheck2 +# network import-check +# timers 60 180 +#! +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +#! +#router bgp 11 +# network import-check +# timers 60 180 +# ! + +- name: "Adds sonic_bgp_neighbors" + dellemc.enterprise_sonic.sonic_bgp_neighbors: + config: + - bgp_as: 51 + neighbors: + - neighbor: Eth1/2 + auth_pwd: + pwd: 'pw123' + encrypted: false + dont_negotiate_capability: true + ebgp_multihop: + enabled: true + multihop_ttl: 1 + enforce_first_as: true + enforce_multihop: true + local_address: 'Ethernet4' + local_as: + as: 2 + no_prepend: true + replace_as: true + nbr_description: "description 1" + override_capability: true + passive: true + port: 3 + shutdown_msg: 'msg1' + solo: true + - neighbor: 1.1.1.1 + disable_connected_check: true + ttl_security: 5 + - bgp_as: 51 + vrf_name: VrfReg1 + peer_group: + - name: SPINE + bfd: + check_failure: true + enabled: true + profile: 'profile 1' + capability: + dynamic: true + extended_nexthop: true + auth_pwd: + pwd: 'U2FsdGVkX1/4sRsZ624wbAJfDmagPLq2LsGDOcW/47M=' + encrypted: true + dont_negotiate_capability: true + ebgp_multihop: + enabled: true + multihop_ttl: 1 + enforce_first_as: true + enforce_multihop: true + local_address: 'Ethernet4' + local_as: + as: 2 + no_prepend: true + replace_as: true + pg_description: 'description 1' + override_capability: true + passive: true + solo: true + remote_as: + peer_as: 4 + - name: SPINE1 + disable_connected_check: true + shutdown_msg: "msg1" + strict_capability_match: true + timers: + keepalive: 30 + holdtime: 15 + connect_retry: 25 + ttl_security: 5 + address_family: + afis: + - afi: ipv4 + safi: unicast + activate: true + allowas_in: + origin: true + - afi: ipv6 + safi: unicast + activate: true + allowas_in: + value: 5 + neighbors: + - neighbor: Eth1/3 + remote_as: + peer_as: 10 + peer_group: SPINE + advertisement_interval: 15 + timers: + keepalive: 30 + holdtime: 15 + connect_retry: 25 + bfd: + check_failure: true + enabled: true + profile: 'profile 1' + capability: + dynamic: true + extended_nexthop: true + auth_pwd: + pwd: 'U2FsdGVkX199MZ7YOPkOR9O6wEZmtGSgiDfnlcN9hBg=' + encrypted: true + nbr_description: 'description 2' + strict_capability_match: true + v6only: true + - neighbor: 192.168.1.4 + state: merged +# +# After state: +# ------------ +#! +#router bgp 11 vrf VrfCheck2 +# network import-check +# timers 60 180 +#! +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +# ! +# peer-group SPINE1 +# timers 15 30 +# timers connect 25 +# shutdown message msg1 +# disable-connected-check +# strict-capability-match +# ttl-security hops 5 +# ! +# peer-group SPINE +# description "description 1" +# ebgp-multihop 1 +# remote-as 4 +# bfd check-control-plane-failure profile "profile 1" +# update-source interface Ethernet4 +# capability dynamic +# capability extended-nexthop +# dont-capability-negotiate +# enforce-first-as +# enforce-multihop +# local-as 2 no-prepend replace-as +# override-capability +# passive +# password U2FsdGVkX1/4sRsZ624wbAJfDmagPLq2LsGDOcW/47M= encrypted +# solo +# address-family ipv4 unicast +# activate +# allowas-in origin +# send-community both +# ! +# address-family ipv6 unicast +# activate +# allowas-in 5 +# send-community both +# ! +# neighbor interface Eth1/3 +# description "description 2" +# peer-group SPINE +# remote-as 10 +# timers 15 30 +# timers connect 25 +# bfd check-control-plane-failure profile "profile 1" +# advertisement-interval 15 +# capability extended-nexthop +# capability dynamic +# v6only +# password U2FsdGVkX199MZ7YOPkOR9O6wEZmtGSgiDfnlcN9hBg= encrypted +# strict-capability-match +# ! +# neighbor 192.168.1.4 +#! +# router bgp 51 +# timers 60 180 +# neighbor interface Eth1/2 +# description "description 1" +# shutdown message msg1 +# ebgp-multihop 1 +# remote-as external +# update-source interface Ethernet4 +# dont-capability-negotiate +# enforce-first-as +# enforce-multihop +# local-as 2 no-prepend replace-as +# override-capability +# passive +# password U2FsdGVkX1+bxMf9TKOhaXRNNaHmywiEVDF2lJ2c000= encrypted +# port 3 +# solo +# neighbor 1.1.1.1 +# disable-connected-check +# ttl-security hops 5 +#router bgp 11 +# network import-check +# timers 60 180 +# +# Using deleted +# +# Before state: +# ------------ +#! +#router bgp 11 vrf VrfCheck2 +# network import-check +# timers 60 180 +#! +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +# ! +# peer-group SPINE +# bfd +# remote-as 4 +# ! +# neighbor interface Eth1/3 +# peer-group SPINE +# remote-as 10 +# timers 15 30 +# advertisement-interval 15 +# bfd +# capability extended-nexthop +# capability dynamic +# ! +# neighbor 192.168.1.4 +#! +#router bgp 11 +# network import-check +# timers 60 18 +# ! +# peer-group SP +# ! +# neighbor interface Eth1/3 +# +- name: "Deletes sonic_bgp_neighbors and peer-groups specific to vrfname" + dellemc.enterprise_sonic.sonic_bgp_neighbors: + config: + - bgp_as: 51 + vrf_name: VrfReg1 + state: deleted + +# After state: +# ------------ +#! +#router bgp 11 vrf VrfCheck2 +# network import-check +# timers 60 180 +#! +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +# ! +#router bgp 11 +# network import-check +# timers 60 18 +# ! +# peer-group SP +# ! +# neighbor interface Eth1/3 +# +# Using deleted +# +# Before state: +# ------------- +# +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +# ! +# peer-group SPINE +# bfd +# remote-as 4 +# ! +# neighbor interface Eth1/3 +# peer-group SPINE +# remote-as 10 +# timers 15 30 +# advertisement-interval 15 +# bfd +# capability extended-nexthop +# capability dynamic +# ! +# neighbor 192.168.1.4 +# ! + +- name: "Deletes specific sonic_bgp_neighbors" + dellemc.enterprise_sonic.sonic_bgp_neighbors: + config: + - bgp_as: 51 + neighbors: + - neighbor: Eth1/2 + auth_pwd: + pwd: 'pw123' + encrypted: false + dont_negotiate_capability: true + ebgp_multihop: + enabled: true + multihop_ttl: 1 + enforce_first_as: true + enforce_multihop: true + local_address: 'Ethernet4' + local_as: + as: 2 + no_prepend: true + replace_as: true + nbr_description: 'description 1' + override_capability: true + passive: true + port: 3 + shutdown_msg: 'msg1' + solo: true + - neighbor: 1.1.1.1 + disable_connected_check: true + ttl_security: 5 + - bgp_as: 51 + vrf_name: VrfReg1 + peer_group: + - name: SPINE + bfd: + check_failure: true + enabled: true + profile: 'profile 1' + capability: + dynamic: true + extended_nexthop: true + auth_pwd: + pwd: 'U2FsdGVkX1/4sRsZ624wbAJfDmagPLq2LsGDOcW/47M=' + encrypted: true + dont_negotiate_capability: true + ebgp_multihop: + enabled: true + multihop_ttl: 1 + enforce_first_as: true + enforce_multihop: true + local_address: 'Ethernet4' + local_as: + as: 2 + no_prepend: true + replace_as: true + pg_description: 'description 1' + override_capability: true + passive: true + solo: true + remote_as: + peer_as: 4 + - name: SPINE1 + disable_connected_check: true + shutdown_msg: "msg1" + strict_capability_match: true + timers: + keepalive: 30 + holdtime: 15 + connect_retry: 25 + ttl_security: 5 + neighbors: + - neighbor: Eth1/3 + remote_as: + peer_as: 10 + peer_group: SPINE + advertisement_interval: 15 + timers: + keepalive: 30 + holdtime: 15 + connect_retry: 25 + bfd: + check_failure: true + enabled: true + profile: 'profile 1' + capability: + dynamic: true + extended_nexthop: true + auth_pwd: + pwd: 'U2FsdGVkX199MZ7YOPkOR9O6wEZmtGSgiDfnlcN9hBg=' + encrypted: true + nbr_description: 'description 2' + strict_capability_match: true + v6only: true + - neighbor: 192.168.1.4 + state: deleted +# +# After state: +# ------------- +# +#router bgp 51 vrf VrfReg1 +# network import-check +# timers 60 180 +# ! +# peer-group SPINE1 +# ! +# peer-group SPINE +# ! +# neighbor interface Eth1/3 +# ! +# neighbor interface Eth1/2 +# neighbor 1.1.1.1 +# +# Using merged +# +# Before state: +# ------------- +# +# sonic# show running-configuration bgp peer-group vrf default +# (No bgp peer-group configuration present) + +- name: "Configure BGP peer-group prefix-list attributes" + dellemc.enterprise_sonic.sonic_bgp_neighbors: + config: + - bgp_as: 51 + peer_group: + - name: SPINE + address_family: + afis: + - afi: ipv4 + safi: unicast + ip_afi: + default_policy_name: rmap_reg1 + send_default_route: true + prefix_limit: + max_prefixes: 1 + prevent_teardown: true + warning_threshold: 80 + prefix_list_in: p1 + prefix_list_out: p2 + state: merged + +# After state: +# ------------ +# +# sonic# show running-configuration bgp peer-group vrf default +# ! +# peer-group SPINE +# ! +# address-family ipv4 unicast +# default-originate route-map rmap_reg1 +# prefix-list p1 in +# prefix-list p2 out +# send-community both +# maximum-prefix 1 80 warning-only +# +# Using deleted +# +# Before state: +# ------------- +# +# sonic# show running-configuration bgp peer-group vrf default +# ! +# peer-group SPINE +# ! +# address-family ipv6 unicast +# default-originate route-map rmap_reg2 +# prefix-list p1 in +# prefix-list p2 out +# send-community both +# maximum-prefix 5 90 restart 2 + +- name: "Delete BGP peer-group prefix-list attributes" + dellemc.enterprise_sonic.sonic_bgp_neighbors: + config: + - bgp_as: 51 + peer_group: + - name: SPINE + address_family: + afis: + - afi: ipv6 + safi: unicast + ip_afi: + default_policy_name: rmap_reg2 + send_default_route: true + prefix_limit: + max_prefixes: 5 + warning_threshold: 90 + restart-timer: 2 + prefix_list_in: p1 + prefix_list_out: p2 + state: deleted + +# sonic# show running-configuration bgp peer-group vrf default +# (No bgp peer-group configuration present) +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.bgp_neighbors.bgp_neighbors import Bgp_neighborsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bgp_neighbors.bgp_neighbors import Bgp_neighbors + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Bgp_neighborsArgs.argument_spec, + supports_check_mode=True) + + result = Bgp_neighbors(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors_af.py new file mode 100644 index 000000000..10400cfe2 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors_af.py @@ -0,0 +1,451 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_bgp_neighbors_af +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_bgp_neighbors_af +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Niraimadaiselvam M (@niraimadaiselvamm) +short_description: Manage the BGP neighbor address-family and its parameters +description: + - This module provides configuration management of BGP neighbors address-family parameters on devices running Enterprise SONiC. + - bgp_as, vrf_name and neighbors need be created in advance on the device. +options: + config: + description: + - Specifies the BGP neighbors address-family related configuration. + type: list + elements: dict + suboptions: + bgp_as: + description: + - Specifies the BGP autonomous system (AS) number which is already configured on the device. + type: str + required: true + vrf_name: + description: + - Specifies the VRF name which is already configured on the device. + type: str + default: 'default' + neighbors: + description: + - Specifies BGP neighbor related configurations in address-family configuration mode. + type: list + elements: dict + suboptions: + neighbor: + description: + - Neighbor router address which is already configured on the device. + type: str + required: True + address_family: + description: + - Specifies BGP address-family related configurations. + - afi and safi are required together. + type: list + elements: dict + suboptions: + afi: + description: + - Type of address-family to configure. + type: str + choices: + - ipv4 + - ipv6 + - l2vpn + required: True + safi: + description: + - Specifies the type of cast for the address-family. + type: str + choices: + - unicast + - evpn + default: unicast + activate: + description: + - Enables the address-family for this neighbor. + type: bool + allowas_in: + description: + - Specifies the allowas in values. + type: dict + suboptions: + value: + description: + - Specifies the value of the allowas in. + type: int + origin: + description: + - Specifies the origin value. + type: bool + ip_afi: + description: + - Common configuration attributes for IPv4 and IPv6 unicast address families. + type: dict + suboptions: + default_policy_name: + description: + - Specifies routing policy definition. + type: str + send_default_route: + description: + - Enable or disable sending of default-route to the neighbor. + type: bool + default: False + prefix_limit: + description: + - Specifies prefix limit attributes. + type: dict + suboptions: + max_prefixes: + description: + - Maximum number of prefixes that will be accepted from the neighbor. + type: int + prevent_teardown: + description: + - Enable or disable teardown of BGP session when maximum prefix limit is exceeded. + type: bool + default: False + warning_threshold: + description: + - Threshold on number of prefixes that can be received from a neighbor before generation of warning messages. + - Expressed as a percentage of max-prefixes. + type: int + restart_timer: + description: + - Time interval in seconds after which the BGP session is re-established after being torn down. + type: int + prefix_list_in: + description: + - Inbound route filtering policy for a neighbor. + type: str + prefix_list_out: + description: + - Outbound route filtering policy for a neighbor. + type: str + route_map: + description: + - Specifies the route-map. + type: list + elements: dict + suboptions: + name: + description: + - Specifies the name of the route-map. + type: str + direction: + description: + - Specifies the direction of the route-map. + type: str + route_reflector_client: + description: + - Specifies a neighbor as a route-reflector client. + type: bool + route_server_client: + description: + - Specifies a neighbor as a route-server client. + type: bool + state: + description: + - Specifies the operation to be performed on the BGP process that is configured on the device. + - In case of merged, the input configuration is merged with the existing BGP configuration on the device. + - In case of deleted, the existing BGP configuration is removed from the device. + default: merged + type: str + choices: ['merged', 'deleted'] +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#! +#router bgp 4 +# ! +# neighbor interface Eth1/3 +# ! +# address-family ipv4 unicast +# activate +# allowas-in 4 +# route-map aa in +# route-map aa out +# route-reflector-client +# route-server-client +# send-community both +#! +# +- name: Deletes neighbors address-family with specific values + dellemc.enterprise_sonic.sonic_bgp_neighbors_af: + config: + - bgp_as: 4 + neighbors: + - neighbor: Eth1/3 + address_family: + - afi: ipv4 + safi: unicast + allowas_in: + value: 4 + route_map: + - name: aa + direction: in + - name: aa + direction: out + route_reflector_client: true + route_server_client: true + state: deleted + +# After state: +# ------------ +#! +#router bgp 4 +# ! +# neighbor interface Eth1/3 +# ! +# address-family ipv4 unicast +# send-community both +#! + + +# Using deleted +# +# Before state: +# ------------- +# +#! +#router bgp 4 +# ! +# neighbor interface Eth1/3 +# ! +# address-family ipv4 unicast +# activate +# allowas-in 4 +# route-map aa in +# route-map aa out +# route-reflector-client +# route-server-client +# send-community both +#! +# neighbor interface Eth1/5 +# ! +# address-family ipv4 unicast +# activate +# allowas-in origin +# send-community both +#! +# +- name: Deletes neighbors address-family with specific values + dellemc.enterprise_sonic.sonic_bgp_neighbors_af: + config: + state: deleted + +# After state: +# ------------ +#! +#router bgp 4 +#! + + +# Using deleted +# +# Before state: +# ------------- +# +#! +#router bgp 4 +# ! +# neighbor interface Eth1/3 +#! +# +- name: Merges neighbors address-family with specific values + dellemc.enterprise_sonic.sonic_bgp_neighbors_af: + config: + - bgp_as: 4 + neighbors: + - neighbor: Eth1/3 + address_family: + - afi: ipv4 + safi: unicast + allowas_in: + value: 4 + route_map: + - name: aa + direction: in + - name: aa + direction: out + route_reflector_client: true + route_server_client: true + state: merged + +# After state: +# ------------ +#! +#router bgp 4 +# ! +# neighbor interface Eth1/3 +# ! +# address-family ipv4 unicast +# activate +# allowas-in 4 +# route-map aa in +# route-map aa out +# route-reflector-client +# route-server-client +# send-community both +#! + + +# Using merged +# +# Before state: +# ------------- +# +# sonic# show running-configuration bgp neighbor vrf default 1.1.1.1 +# (No bgp neighbor configuration present) +- name: "Configure BGP neighbor prefix-list attributes" + dellemc.enterprise_sonic.sonic_bgp_neighbors_af: + config: + - bgp_as: 51 + neighbors: + - neighbor: 1.1.1.1 + address_family: + - afi: ipv4 + safi: unicast + ip_afi: + default_policy_name: rmap_reg1 + send_default_route: true + prefix_limit: + max_prefixes: 1 + prevent_teardown: true + warning_threshold: 80 + prefix_list_in: p1 + prefix_list_out: p2 + state: merged +# After state: +# ------------ +# +# sonic# show running-configuration bgp neighbor vrf default 1.1.1.1 +# ! +# neighbor 1.1.1.1 +# ! +# address-family ipv4 unicast +# default-originate route-map rmap_reg1 +# prefix-list p1 in +# prefix-list p2 out +# send-community both +# maximum-prefix 1 80 warning-only + + +# Using deleted +# +# Before state: +# ------------- +# +# sonic# show running-configuration bgp neighbor vrf default 1.1.1.1 +# ! +# neighbor 1.1.1.1 +# ! +# address-family ipv6 unicast +# default-originate route-map rmap_reg2 +# prefix-list p1 in +# prefix-list p2 out +# send-community both +# maximum-prefix 5 90 restart 2 +- name: "Delete BGP neighbor prefix-list attributes" + dellemc.enterprise_sonic.sonic_bgp_neighbors_af: + config: + - bgp_as: 51 + neighbors: + - neighbor: 1.1.1.1 + address_family: + - afi: ipv6 + safi: unicast + ip_afi: + default_policy_name: rmap_reg2 + send_default_route: true + prefix_limit: + max_prefixes: 5 + warning_threshold: 90 + restart-timer: 2 + prefix_list_in: p1 + prefix_list_out: p2 + state: deleted +# sonic# show running-configuration bgp neighbor vrf default 1.1.1.1 +# (No bgp neighbor configuration present) +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.bgp_neighbors_af.bgp_neighbors_af import Bgp_neighbors_afArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bgp_neighbors_af.bgp_neighbors_af import Bgp_neighbors_af + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Bgp_neighbors_afArgs.argument_spec, + supports_check_mode=True) + + result = Bgp_neighbors_af(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_command.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_command.py new file mode 100644 index 000000000..b0ce87811 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_command.py @@ -0,0 +1,235 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020, Peter Sprygada <psprygada@ansible.com> +# Copyright: (c) 2020, Dell Inc. +# 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 = """ +--- +module: sonic_command +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Dhivya P (@dhivayp) +short_description: Runs commands on devices running Enterprise SONiC +description: + - Runs commands on remote devices running Enterprise SONiC Distribution + by Dell Technologies. Sends arbitrary commands to an Enterprise SONiC node and + returns the results that are read from the device. This module includes an + argument that causes the module to wait for a specific condition + before returning or time out if the condition is not met. + - This module does not support running commands in configuration mode. + To configure SONiC devices, use M(dellemc.enterprise_sonic.sonic_config). +options: + commands: + description: + - List of commands to send to the remote Enterprise SONiC devices over the + configured provider. The resulting output from the command + is returned. If the I(wait_for) argument is provided, the + module is not returned until the condition is satisfied or + the number of retries has expired. If a command sent to the + device requires answering a prompt, it is possible to pass + a dict containing I(command), I(answer) and I(prompt). + Common answers are 'yes' or "\\r" (carriage return, must be + double quotes). See examples. + type: list + elements: str + required: true + wait_for: + description: + - List of conditions to evaluate against the output of the + command. The task waits for each condition to be true + before moving forward. If the conditional is not true + within the configured number of retries, the task fails. + See examples. + type: list + elements: str + match: + description: + - The I(match) argument is used in conjunction with the + I(wait_for) argument to specify the match policy. Valid + values are C(all) or C(any). If the value is set to C(all) + then all conditionals in the wait_for must be satisfied. If + the value is set to C(any) then only one of the values must be + satisfied. + type: str + default: all + choices: [ 'all', 'any' ] + retries: + description: + - Specifies the number of retries a command should be run + before it is considered failed. The command is run on the + target device every retry and evaluated against the + I(wait_for) conditions. + type: int + default: 10 + interval: + description: + - Configures the interval in seconds to wait between retries + of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before + trying the command again. + type: int + default: 1 +""" + +EXAMPLES = """ + - name: Runs show version on remote devices + dellemc.enterprise_sonic.sonic_command: + commands: show version + + - name: Runs show version and checks to see if output contains 'Dell' + dellemc.enterprise_sonic.sonic_command: + commands: show version + wait_for: result[0] contains Dell + + - name: Runs multiple commands on remote nodes + dellemc.enterprise_sonic.sonic_command: + commands: + - show version + - show interface + + - name: Runs multiple commands and evaluate the output + dellemc.enterprise_sonic.sonic_command: + commands: + - 'show version' + - 'show system' + wait_for: + - result[0] contains Dell + - result[1] contains Hostname + + - name: Runs commands that require answering a prompt + dellemc.enterprise_sonic.sonic_command: + commands: + - command: 'reload' + prompt: '[confirm yes/no]: ?$' + answer: 'no' +""" + +RETURN = """ +stdout: + description: The set of responses from the commands. + returned: always apart from low level errors (such as action plugin) + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list. + returned: always apart from low level errors (such as action plugin) + type: list + sample: [['...', '...'], ['...'], ['...']] +failed_conditions: + description: The list of conditionals that have failed. + returned: failed + type: list + sample: ['...', '...'] +warnings: + description: The list of warnings (if any) generated by module based on arguments. + returned: always + type: list + sample: ['...', '...'] +""" +import time + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import ( + Conditional, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + EntityCollection, + to_lines, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import run_commands +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import command_list_str_to_dict + + +def transform_commands_dict(module, commands_dict): + transform = EntityCollection( + module, + dict( + command=dict(key=True), + output=dict(), + prompt=dict(type="list"), + answer=dict(type="list"), + newline=dict(type="bool", default=True), + sendonly=dict(type="bool", default=False), + check_all=dict(type="bool", default=False), + ), + ) + + return transform(commands_dict) + + +def parse_commands(module, warnings): + commands_dict = command_list_str_to_dict(module, warnings, module.params["commands"]) + commands = transform_commands_dict(module, commands_dict) + return commands + + +def main(): + """main entry point for module execution + """ + argument_spec = dict( + # { command: <str>, prompt: <str>, response: <str> } + commands=dict(type='list', required=True, elements="str"), + + wait_for=dict(type='list', elements="str"), + match=dict(default='all', choices=['all', 'any']), + + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + result = {'changed': False} + + warnings = list() +# check_args(module, warnings) + commands = parse_commands(module, warnings) + result['warnings'] = warnings + + wait_for = module.params['wait_for'] or list() + try: + conditionals = [Conditional(c) for c in wait_for] + except AttributeError as exc: + module.fail_json(msg=to_text(exc)) + retries = module.params['retries'] + interval = module.params['interval'] + match = module.params['match'] + + while retries > 0: + responses = run_commands(module, commands) + for item in list(conditionals): + if item(responses): + if match == 'any': + conditionals = list() + break + conditionals.remove(item) + + if not conditionals: + break + + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = 'One or more conditional statements have not been satisfied.' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + result.update({ + 'stdout': responses, + 'stdout_lines': list(to_lines(responses)) + }) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_config.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_config.py new file mode 100644 index 000000000..dd054419f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_config.py @@ -0,0 +1,329 @@ +#!/usr/bin/python +# +# (c) 2015 Peter Sprygada, <psprygada@ansible.com> +# Copyright (c) 2020 Dell Inc. +# +# 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 = """ +--- +module: sonic_config +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Abirami N (@abirami-n) +short_description: Manages configuration sections on devices running Enterprise SONiC +description: + - Manages configuration sections of Enterprise SONiC Distribution + by Dell Technologies. SONiC configurations use a simple block indent + file syntax for segmenting configuration into sections. This module + provides an implementation for working with SONiC configuration + sections in a deterministic way. +options: + lines: + description: + - The ordered set of commands that should be configured in the + section. The commands must be the exact same commands as found + in the device running-configuration. Be sure to note the configuration + command syntax as some commands are automatically modified by the + device configuration parser. This argument is mutually exclusive + with I(src). + type: list + elements: str + aliases: ['commands'] + parents: + description: + - The ordered set of parents that uniquely identify the section or hierarchy + the commands should be checked against. If the parents argument + is omitted, the commands are checked against the set of top + level or global commands. + type: list + elements: str + src: + description: + - Specifies the source path to the file that contains the configuration + or configuration template to load. The path to the source file can + either be the full path on the Ansible control host, or a relative + path from the playbook or role root directory. This argument is + mutually exclusive with I(lines). + type: path + before: + description: + - The ordered set of commands to push on to the command stack if + a change needs to be made. This allows the playbook designer + the opportunity to perform configuration commands prior to pushing + any changes without affecting how the set of commands are matched + against the system. + type: list + elements: str + after: + description: + - The ordered set of commands to append to the end of the command + stack if a change needs to be made. Just like with I(before), this + allows the playbook designer to append a set of commands to be + executed after the command set. + type: list + elements: str + save: + description: + - The C(save) argument instructs the module to save the running- + configuration to the startup-configuration at the conclusion of + the module running. If check mode is specified, this argument + is ignored. + type: bool + default: 'no' + match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device configuration. + If match is set to I(line), commands are matched line by line. + If match is set to I(strict), command lines are matched with respect + to position. If match is set to I(exact), command lines + must be an equal match. If match is set to I(none), the + module does not attempt to compare the source configuration with + the running-configuration on the remote device. + type: str + default: line + choices: ['line', 'strict', 'exact', 'none'] + replace: + description: + - Instructs the module how to perform a configuration + on the device. If the replace argument is set to I(line), then + the modified lines are pushed to the device in configuration + mode. If the replace argument is set to I(block), then the entire + command block is pushed to the device in configuration mode if any + line is not correct. + type: str + default: line + choices: ['line', 'block'] + update: + description: + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) and I(check). When you set this argument to + I(merge), the configuration changes merge with the current + device running-configuration. When you set this argument to I(check), + the configuration updates are determined but not configured + on the remote device. + type: str + default: merge + choices: ['merge', 'check'] + config: + description: + - The module, by default, connects to the remote device and + retrieves the current running-configuration to use as a base for + comparing against the contents of source. There are times when + it is not desirable to have the task get the current + running-configuration for every task in a playbook. The I(config) + argument allows the implementer to pass in the configuration to + use as the base configuration for comparison. + type: str + backup: + description: + - This argument causes the module to create a full backup of + the current C(running-configuration) from the remote device before any + changes are made. If the C(backup_options) value is not given, + the backup file is written to the C(backup) folder in the playbook + root directory. If the directory does not exist, it is created. + type: bool + default: 'no' + backup_options: + description: + - This is a dictionary object containing configurable options related to backup file path. + The value of this option is read only when C(backup) is set to I(yes), if C(backup) is set + to I(no) this option is ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given, it is generated based on the hostname, current time, and date + in the format defined by <hostname>_config.<current-date>@<current-time>. + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file is stored. If the directory does not exist it is first + created, and the filename is either the value of C(filename) or default filename + as described in C(filename) options description. If the path value is not given, + an I(backup) directory is created in the current working directory + and backup configuration is copied in C(filename) within the I(backup) directory. + type: path + type: dict +""" + +EXAMPLES = """ +- dellemc.enterprise_sonic.sonic_config: + lines: ['username {{ user_name }} password {{ user_password }} role {{ user_role }}'] + +- dellemc.enterprise_sonic.sonic_config: + lines: + - description 'SONiC' + parents: ['interface Eth1/10'] + +- dellemc.enterprise_sonic.sonic_config: + lines: + - seq 2 permit udp any any + - seq 3 deny icmp any any + parents: ['ip access-list test'] + before: ['no ip access-list test'] + +""" + +RETURN = """ +updates: + description: The set of commands that is pushed to the remote device. + returned: always + type: list + sample: ['username foo password foo role admin', 'router bgp 1', 'router-id 1.1.1.1'] +commands: + description: The set of commands that is pushed to the remote device. + returned: always + type: list + sample: ['username foo password foo role admin', 'router bgp 1', 'router-id 1.1.1.1'] +saved: + description: Returns whether the configuration is saved to the startup + configuration or not. + returned: When not check_mode. + type: bool + sample: True +""" + +from ansible.module_utils.connection import ConnectionError + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import get_config, get_sublevel_config +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import edit_config, run_commands +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import command_list_str_to_dict +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import NetworkConfig, dumps + + +def get_candidate(module): + candidate = NetworkConfig(indent=1) + if module.params['src']: + candidate.load(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + commands = module.params['lines'][0] + if (isinstance(commands, dict)) and (isinstance((commands['command']), list)): + candidate.add(commands['command'], parents=parents) + elif (isinstance(commands, dict)) and (isinstance((commands['command']), str)): + candidate.add([commands['command']], parents=parents) + else: + candidate.add(module.params['lines'], parents=parents) + return candidate + + +def get_running_config(module): + contents = module.params['config'] + if not contents: + contents = get_config(module) + return contents + + +def main(): + + backup_spec = dict( + filename=dict(), + dir_path=dict(type='path') + ) + + argument_spec = dict( + lines=dict(aliases=['commands'], type='list', elements="str"), + parents=dict(type='list', elements="str"), + + src=dict(type='path'), + + before=dict(type='list', elements="str"), + after=dict(type='list', elements="str"), + save=dict(type='bool', default=False), + match=dict(default='line', + choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), + + update=dict(choices=['merge', 'check'], default='merge'), + config=dict(), + backup=dict(type='bool', default=False), + backup_options=dict(type='dict', options=backup_spec) + + ) + + mutually_exclusive = [('lines', 'src')] + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + parents = module.params['parents'] or list() + match = module.params['match'] + replace = module.params['replace'] + + warnings = list() +# check_args(module, warnings) + + result = dict(changed=False, saved=False, warnings=warnings) + if module.params['backup']: + if not module.check_mode: + result['__backup__'] = get_config(module) + + commands = list() + candidate = get_candidate(module) + if any((module.params['lines'], module.params['src'])): + if match != 'none': + config = get_running_config(module) + if parents: + contents = get_sublevel_config(config, module) + config = NetworkConfig(contents=contents, indent=1) + else: + config = NetworkConfig(contents=config, indent=1) + configobjs = candidate.difference(config, match=match, replace=replace) + else: + + configobjs = candidate.items + if configobjs: + commands = dumps(configobjs, 'commands') + if ((isinstance((module.params['lines']), list)) and + (isinstance((module.params['lines'][0]), dict)) and + (set(['prompt', 'answer']).issubset(module.params['lines'][0]))): + + cmd = {'command': commands, + 'prompt': module.params['lines'][0]['prompt'], + 'answer': module.params['lines'][0]['answer']} + commands = [cmd] + else: + commands = commands.split('\n') + cmd_list_out = command_list_str_to_dict(module, warnings, commands) + if cmd_list_out and cmd_list_out != []: + commands = cmd_list_out + + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + if not module.check_mode and module.params['update'] == 'merge': + try: + edit_config(module, commands) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc)) + + result['changed'] = True + result['commands'] = commands + result['updates'] = commands + + if module.params['save']: + result['changed'] = True + if not module.check_mode: + cmd = {r'command': ' write memory'} + run_commands(module, [cmd]) + result['saved'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_facts.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_facts.py new file mode 100644 index 000000000..f13e9defd --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_facts.py @@ -0,0 +1,136 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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 module file for sonic_facts +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_facts +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Collects facts on devices running Enterprise SONiC +description: + - Collects facts from devices running Enterprise SONiC Distribution by + Dell Technologies. This module places the facts gathered in the fact tree + keyed by the respective resource name. The facts module always collects + a base set of facts from the device and can enable or disable collection + of additional facts. +author: +- Mohamed Javeed (@javeedf) +- Abirami N (@abirami-n) +options: + gather_subset: + description: + - When supplied, this argument restricts the facts collected + to a given subset. Possible values for this argument include + all, min, hardware, config, legacy, and interfaces. Can specify a + list of values to include a larger subset. Values can also be used + with an initial '!' to specify that a specific subset should + not be collected. + required: false + type: list + elements: str + default: '!config' + gather_network_resources: + description: + - When supplied, this argument restricts the facts collected + to a given subset. Possible values for this argument include + all and the resources like 'all', 'interfaces', 'vlans', 'lag_interfaces', 'l2_interfaces', 'l3_interfaces'. + Can specify a list of values to include a larger subset. Values + can also be used with an initial '!' to specify that a + specific subset should not be collected. + required: false + type: list + elements: str + choices: + - all + - vlans + - interfaces + - l2_interfaces + - l3_interfaces + - lag_interfaces + - bgp + - bgp_af + - bgp_neighbors + - bgp_neighbors_af + - bgp_as_paths + - bgp_communities + - bgp_ext_communities + - mclag + - prefix_lists + - vrfs + - vxlans + - users + - system + - port_breakout + - aaa + - tacacs_server + - radius_server + - static_routes + - ntp +""" + +EXAMPLES = """ +- name: Gather all facts + dellemc.enterprise_sonic.sonic_facts: + gather_subset: all + gather_network_resources: all +- name: Collects VLAN and interfaces facts + dellemc.enterprise_sonic.sonic_facts: + gather_subset: + - min + gather_network_resources: + - vlans + - interfaces +- name: Do not collects VLAN and interfaces facts + dellemc.enterprise_sonic.sonic_facts: + gather_network_resources: + - "!vlans" + - "!interfaces" +- name: Collects VLAN and minimal default facts + dellemc.enterprise_sonic.sonic_facts: + gather_subset: min + gather_network_resources: vlans +- name: Collect lag_interfaces and minimal default facts + dellemc.enterprise_sonic.sonic_facts: + gather_subset: min + gather_network_resources: lag_interfaces +""" + +RETURN = """ +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.facts.facts import FactsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import Facts + + +def main(): + """ + Main entry point for module execution + :returns: ansible_facts + """ + module = AnsibleModule(argument_spec=FactsArgs.argument_spec, + supports_check_mode=True) + warnings = ['default value for `gather_subset` ' + 'will be changed to `min` from `!config` v2.11 onwards'] + + result = Facts(module).get_facts() + + ansible_facts, additional_warnings = result + warnings.extend(additional_warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_interfaces.py new file mode 100644 index 000000000..0cd6a1896 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_interfaces.py @@ -0,0 +1,230 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2020 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_interfaces +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_interfaces +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Configure Interface attributes on interfaces such as, Eth, LAG, VLAN, and loopback. + (create a loopback interface if it does not exist.) +description: Configure Interface attributes such as, MTU, admin statu, and so on, on interfaces + such as, Eth, LAG, VLAN, and loopback. (create a loopback interface if it does not exist.) +author: Niraimadaiselvam M(@niraimadaiselvamm) +options: + config: + description: A list of interface configurations. + type: list + elements: dict + suboptions: + name: + type: str + description: The name of the interface, for example, 'Eth1/15'. + required: true + description: + type: str + description: + - Description about the interface. + enabled: + description: + - Administrative state of the interface. + type: bool + mtu: + description: + - MTU of the interface. + type: int + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper Speed MTU +#------------------------------------------------------------------------------------------ +#Eth1/1 - up 100000 9100 +#Eth1/2 - up 100000 9100 +#Eth1/3 - down 100000 9100 +#Eth1/3 - down 1000 5000 +#Eth1/5 - down 100000 9100 +# +- name: Configures interfaces + dellemc.enterprise_sonic.sonic_interfaces: + config: + name: Eth1/3 + state: deleted +# +# After state: +# ------------- +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper Speed MTU +#------------------------------------------------------------------------------------------ +#Eth1/1 - up 100000 9100 +#Eth1/2 - up 100000 9100 +#Eth1/3 - down 100000 9100 +#Eth1/3 - up 100000 9100 +#Eth1/5 - down 100000 9100 +# +# +# Using deleted +# +# Before state: +# ------------- +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper Speed MTU +#------------------------------------------------------------------------------------------ +#Eth1/1 - up 100000 9100 +#Eth1/2 - up 100000 9100 +#Eth1/3 - down 100000 9100 +#Eth1/3 - down 1000 9100 +#Eth1/5 - down 100000 9100 +# + +- name: Configures interfaces + dellemc.enterprise_sonic.sonic_interfaces: + config: + state: deleted + +# +# After state: +# ------------- +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper Speed MTU +#------------------------------------------------------------------------------------------ +#Eth1/1 - up 100000 9100 +#Eth1/2 - up 100000 9100 +#Eth1/3 - up 100000 9100 +#Eth1/3 - up 100000 9100 +#Eth1/5 - up 100000 9100 +# +# +# Using merged +# +# Before state: +# ------------- +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper Speed MTU +#------------------------------------------------------------------------------------------ +#Eth1/1 - up 100000 9100 +#Eth1/2 - up 100000 9100 +#Eth1/3 - down 100000 9100 +#Eth1/3 - down 1000 9100 +# +- name: Configures interfaces + dellemc.enterprise_sonic.sonic_interfaces: + config: + - name: Eth1/3 + description: 'Ethernet Twelve' + - name: Eth1/5 + description: 'Ethernet Sixteen' + enable: True + mtu: 3500 + state: merged +# +# +# After state: +# ------------ +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper Speed MTU +#------------------------------------------------------------------------------------------ +#Eth1/1 - up 100000 9100 +#Eth1/2 - up 100000 9100 +#Eth1/3 - down 100000 9100 +#Eth1/4 - down 1000 9100 +#Eth1/5 - down 100000 3500 +# +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.interfaces.interfaces import InterfacesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.interfaces.interfaces import Interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=InterfacesArgs.argument_spec, + supports_check_mode=True) + + result = Interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_interfaces.py new file mode 100644 index 000000000..34a8ff720 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_interfaces.py @@ -0,0 +1,296 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2020 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_l2_interfaces +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_l2_interfaces +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Configure interface-to-VLAN association that is based on access or trunk mode +description: Manages Layer 2 interface attributes of Enterprise SONiC Distribution by Dell Technologies. +author: Niraimadaiselvam M(@niraimadaiselvamm) +options: + config: + description: A list of Layer 2 interface configurations. + type: list + elements: dict + suboptions: + name: + type: str + description: Full name of the interface, for example, 'Eth1/26'. + required: true + trunk: + type: dict + description: Configures trunking parameters on an interface. + suboptions: + allowed_vlans: + description: Specifies list of allowed VLANs of trunk mode on the interface. + type: list + elements: dict + suboptions: + vlan: + type: int + description: Configures the specified VLAN in trunk mode. + access: + type: dict + description: Configures access mode characteristics of the interface. + suboptions: + vlan: + type: int + description: Configures the specified VLAN in access mode. + state: + type: str + description: The state that the configuration should be left in. + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Eth1/3 +#11 Inactive T Eth1/3 +#12 Inactive A Eth1/4 +#13 Inactive T Eth1/4 +#14 Inactive A Eth1/5 +#15 Inactive T Eth1/5 +# +- name: Configures switch port of interfaces + dellemc.enterprise_sonic.sonic_l2_interfaces: + config: + - name: Eth1/3 + - name: Eth1/4 + state: deleted +# +# After state: +# ------------ +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#11 Inactive +#12 Inactive +#13 Inactive +#14 Inactive A Eth1/5 +#15 Inactive T Eth1/5 +# +# +# Using deleted +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Eth1/3 +#11 Inactive T Eth1/3 +#12 Inactive A Eth1/4 +#13 Inactive T Eth1/4 +#14 Inactive A Eth1/5 +#15 Inactive T Eth1/5 +# +- name: Configures switch port of interfaces + dellemc.enterprise_sonic.sonic_l2_interfaces: + config: + state: deleted +# +# After state: +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#11 Inactive +#12 Inactive +#13 Inactive +#14 Inactive +#15 Inactive +# +# +# Using merged +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#11 Inactive T Eth1/7 +#12 Inactive T Eth1/7 +# +- name: Configures switch port of interfaces + dellemc.enterprise_sonic.sonic_l2_interfaces: + config: + - name: Eth1/3 + access: + vlan: 10 + state: merged +# +# After state: +# ------------ +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Eth1/3 +#11 Inactive T Eth1/7 +#12 Inactive T Eth1/7 +# +# +# Using merged +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Eth1/3 +# +- name: Configures switch port of interfaces + dellemc.enterprise_sonic.sonic_l2_interfaces: + config: + - name: Eth1/3 + trunk: + allowed_vlans: + - vlan: 11 + - vlan: 12 + state: merged +# +# After state: +# ------------ +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Eth1/3 +#11 Inactive T Eth1/7 +#12 Inactive T Eth1/7 +# +# +# Using merged +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#11 Inactive +#12 Inactive A Eth1/4 +#13 Inactive T Eth1/4 +#14 Inactive A Eth1/5 +#15 Inactive T Eth1/5 +# +- name: Configures switch port of interfaces + dellemc.enterprise_sonic.sonic_l2_interfaces: + config: + - name: Eth1/3 + access: + vlan: 12 + trunk: + allowed_vlans: + - vlan: 13 + - vlan: 14 + state: merged +# +# After state: +# ------------ +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#11 Inactive +#12 Inactive A Eth1/3 +# A Eth1/4 +#13 Inactive T Eth1/3 +# T Eth1/4 +#14 Inactive A Eth1/3 +# A Eth1/5 +#15 Inactive T Eth1/5 +# +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.l2_interfaces.l2_interfaces import L2_interfacesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.l2_interfaces.l2_interfaces import L2_interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=L2_interfacesArgs.argument_spec, + supports_check_mode=True) + + result = L2_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_interfaces.py new file mode 100644 index 000000000..e796897a5 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_interfaces.py @@ -0,0 +1,375 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_l3_interfaces +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_l3_interfaces +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Configure the IPv4 and IPv6 parameters on Interfaces such as, Eth, LAG, VLAN, and loopback +description: + - Configures Layer 3 interface settings on devices running Enterprise SONiC + Distribution by Dell Technologies. This module provides configuration management + of IPv4 and IPv6 parameters on Ethernet interfaces of devices running Enterprise SONiC. +author: Kumaraguru Narayanan (@nkumaraguru) +options: + config: + description: A list of l3_interfaces configurations. + type: list + elements: dict + suboptions: + name: + required: True + type: str + description: + - Full name of the interface, for example, Eth1/3. + ipv4: + description: + - ipv4 configurations to be set for the Layer 3 interface mentioned in name option. + type: dict + suboptions: + addresses: + description: + - List of IPv4 addresses to be set. + type: list + elements: dict + suboptions: + address: + description: + - IPv4 address to be set in the format <ipv4 address>/<mask> + for example, 192.0.2.1/24. + type: str + secondary: + description: + - secondary flag of the ip address. + type: bool + default: 'False' + anycast_addresses: + description: + - List of IPv4 addresses to be set for anycast. + type: list + elements: str + ipv6: + description: + - ipv6 configurations to be set for the Layer 3 interface mentioned in name option. + type: dict + suboptions: + addresses: + description: + - List of IPv6 addresses to be set. + type: list + elements: dict + suboptions: + address: + description: + - IPv6 address to be set in the address format is <ipv6 address>/<mask> + for example, 2001:db8:2201:1::1/64. + type: str + enabled: + description: + - enabled flag of the ipv6. + type: bool + state: + description: + - The state that the configuration should be left in. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ + +# Using deleted +# +# Before state: +# ------------- +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 83.1.1.1/16 +# ip address 84.1.1.1/16 secondary +# ipv6 address 83::1/16 +# ipv6 address 84::1/16 +# ipv6 enable +#! +#interface Ethernet24 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 91.1.1.1/16 +# ip address 92.1.1.1/16 secondary +# ipv6 address 90::1/16 +# ipv6 address 91::1/16 +# ipv6 address 92::1/16 +# ipv6 address 93::1/16 +#! +#interface Vlan501 +# ip anycast-address 11.12.13.14/12 +# ip anycast-address 1.2.3.4/22 +#! +# +# +- name: delete one l3 interface. + dellemc.enterprise_sonic.sonic_l3_interfaces: + config: + - name: Ethernet20 + ipv4: + addresses: + - address: 83.1.1.1/16 + - address: 84.1.1.1/16 + - name: Ethernet24 + ipv6: + enabled: true + addresses: + - address: 91::1/16 + - name: Vlan501 + ipv4: + anycast_addresses: + - 11.12.13.14/12 + state: deleted + +# After state: +# ------------ +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +# ipv6 address 83::1/16 +# ipv6 address 84::1/16 +# ipv6 enable +#! +#interface Ethernet24 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 91.1.1.1/16 +# ip address 92.1.1.1/16 secondary +# ipv6 address 90::1/16 +# ipv6 address 92::1/16 +# ipv6 address 93::1/16 +#! +#interface Vlan501 +# ip anycast-address 1.2.3.4/22 +#! +# +# Using deleted +# +# Before state: +# ------------- +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 83.1.1.1/16 +# ip address 84.1.1.1/16 secondary +# ipv6 address 83::1/16 +# ipv6 address 84::1/16 +# ipv6 enable +#! +#interface Ethernet24 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 91.1.1.1/16 +# ipv6 address 90::1/16 +# ipv6 address 91::1/16 +# ipv6 address 92::1/16 +# ipv6 address 93::1/16 +#! +#interface Vlan501 +# ip anycast-address 11.12.13.14/12 +# ip anycast-address 1.2.3.4/22 +#! +# +# +- name: delete all l3 interface + dellemc.enterprise_sonic.sonic_l3_interfaces: + config: + state: deleted +# +# After state: +# ------------ +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +#! +#interface Ethernet24 +# mtu 9100 +# speed 100000 +# shutdown +#! +#interface Vlan501 +#! +# +# Using merged +# +# Before state: +# ------------- +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +#! +#interface Ethernet24 +# mtu 9100 +# speed 100000 +# shutdown +#! +#interface Vlan501 +# ip anycast-address 1.2.3.4/22 +#! +# +- name: Add l3 interface configurations + dellemc.enterprise_sonic.sonic_l3_interfaces: + config: + - name: Ethernet20 + ipv4: + addresses: + - address: 83.1.1.1/16 + - address: 84.1.1.1/16 + secondary: True + ipv6: + enabled: true + addresses: + - address: 83::1/16 + - address: 84::1/16 + secondary: True + - name: Ethernet24 + ipv4: + addresses: + - address: 91.1.1.1/16 + ipv6: + addresses: + - address: 90::1/16 + - address: 91::1/16 + - address: 92::1/16 + - address: 93::1/16 + - name: Vlan501 + ipv4: + anycast_addresses: + - 11.12.13.14/12 + state: merged +# +# After state: +# ------------ +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 83.1.1.1/16 +# ip address 84.1.1.1/16 secondary +# ipv6 address 83::1/16 +# ipv6 address 84::1/16 +# ipv6 enable +#! +#interface Ethernet24 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 91.1.1.1/16 +# ipv6 address 90::1/16 +# ipv6 address 91::1/16 +# ipv6 address 92::1/16 +# ipv6 address 93::1/16 +#! +#interface Vlan501 +# ip anycast-address 1.2.3.4/22 +# ip anycast-address 11.12.13.14/12 +#! +# +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.l3_interfaces.l3_interfaces import L3_interfacesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.l3_interfaces.l3_interfaces import L3_interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=L3_interfacesArgs.argument_spec, + supports_check_mode=True) + + result = L3_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lag_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lag_interfaces.py new file mode 100644 index 000000000..630db7985 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lag_interfaces.py @@ -0,0 +1,238 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_lag_interfaces +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_lag_interfaces +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage link aggregation group (LAG) interface parameters +description: + - This module manages attributes of link aggregation group (LAG) interfaces of + devices running Enterprise SONiC Distribution by Dell Technologies. +author: Abirami N (@abirami-n) + +options: + config: + description: A list of LAG configurations. + type: list + elements: dict + suboptions: + name: + description: + - ID of the LAG. + type: str + required: True + members: + description: + - The list of interfaces that are part of the group. + type: dict + suboptions: + interfaces: + description: The list of interfaces that are part of the group. + type: list + elements: dict + suboptions: + member: + description: + - The interface name. + type: str + mode: + description: + - Specifies mode of the port-channel while creation. + type: str + choices: + - static + - lacp + state: + description: + - The state that the configuration should be left in. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using merged +# +# Before state: +# ------------- +# +# interface Eth1/10 +# mtu 9100 +# speed 100000 +# no shutdown +# ! +# interface Eth1/15 +# channel-group 12 +# mtu 9100 +# speed 100000 +# no shutdown +# +- name: Merges provided configuration with device configuration + dellemc.enterprise_sonic.sonic_lag_interfaces: + config: + - name: PortChannel10 + members: + interfaces: + - member: Eth1/10 + state: merged +# +# After state: +# ------------ +# +# interface Eth1/10 +# channel-group 10 +# mtu 9100 +# speed 100000 +# no shutdown +# ! +# interface Eth1/15 +# channel-group 12 +# mtu 9100 +# speed 100000 +# no shutdown +# +# Using deleted +# +# Before state: +# ------------- +# interface PortChannel10 +# ! +# interface Eth1/10 +# channel-group 10 +# mtu 9100 +# speed 100000 +# no shutdown +# +- name: Deletes LAG attributes of a given interface, This does not delete the port-channel itself + dellemc.enterprise_sonic.sonic_lag_interfaces: + config: + - name: PortChannel10 + members: + interfaces: + state: deleted +# +# After state: +# ------------ +# interface PortChannel10 +# ! +# interface Eth1/10 +# mtu 9100 +# speed 100000 +# no shutdown +# +# Using deleted +# +# Before state: +# ------------- +# interface PortChannel 10 +# ! +# interface PortChannel 12 +# ! +# interface Eth1/10 +# channel-group 10 +# mtu 9100 +# speed 100000 +# no shutdown +# ! +# interface Eth1/15 +# channel-group 12 +# mtu 9100 +# speed 100000 +# no shutdown +# +- name: Deletes all LAGs and LAG attributes of all interfaces + dellemc.enterprise_sonic.sonic_lag_interfaces: + config: + state: deleted +# +# After state: +# ------------- +# +# interface Eth1/10 +# mtu 9100 +# speed 100000 +# no shutdown +# ! +# interface Eth1/15 +# mtu 9100 +# speed 100000 +# no shutdown +# +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration that is returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.lag_interfaces.lag_interfaces import Lag_interfacesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.lag_interfaces.lag_interfaces import Lag_interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Lag_interfacesArgs.argument_spec, + supports_check_mode=True) + + result = Lag_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mclag.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mclag.py new file mode 100644 index 000000000..28d3dbb5b --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mclag.py @@ -0,0 +1,516 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_mclag +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_mclag +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage multi chassis link aggregation groups domain (MCLAG) and its parameters +description: + - Manage multi chassis link aggregation groups domain (MCLAG) and its parameters +author: Abirami N (@abirami-n) + +options: + config: + description: Dict of mclag domain configurations. + type: dict + suboptions: + domain_id: + description: + - ID of the mclag domain (MCLAG domain). + type: int + required: True + peer_address: + description: + - The IPV4 peer-ip for corresponding MCLAG. + type: str + source_address: + description: + - The IPV4 source-ip for corresponding MCLAG. + type: str + peer_link: + description: + - Peer-link for corresponding MCLAG. + type: str + system_mac: + description: + - Mac address of MCLAG. + type: str + keepalive: + description: + - MCLAG session keepalive-interval in secs. + type: int + session_timeout: + description: + - MCLAG session timeout value in secs. + type: int + unique_ip: + description: Holds Vlan dictionary for mclag unique ip. + suboptions: + vlans: + description: + - Holds list of VLANs for which a separate IP addresses is enabled for Layer 3 protocol support over MCLAG. + type: list + elements: dict + suboptions: + vlan: + description: Holds a VLAN ID. + type: str + type: dict + members: + description: Holds portchannels dictionary for an MCLAG domain. + suboptions: + portchannels: + description: + - Holds a list of portchannels for configuring as an MCLAG interface. + type: list + elements: dict + suboptions: + lag: + description: Holds a PortChannel ID. + type: str + type: dict + state: + description: + - The state that the configuration should be left in. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using merged +# +# Before state: +# ------------- +# +# sonic# show mclag brief +# MCLAG Not Configured +# +- name: Merge provided configuration with device configuration + dellemc.enterprise_sonic.sonic_mclag: + config: + domain_id: 1 + peer_address: 1.1.1.1 + source_address: 2.2.2.2 + peer_link: 'Portchannel1' + keepalive: 1 + session_timeout: 3 + unique_ip: + vlans: + - vlan: Vlan4 + members: + portchannles: + - lag: PortChannel10 + state: merged +# +# After state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : 2.2.2.2 +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 1 secs +# Session Timeout : 3 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:1 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +# +# +# Using merged +# +# Before state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : 2.2.2.2 +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 1 secs +# Session Timeout : 3 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:1 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +# +# +- name: Merge device configuration with the provided configuration + dellemc.enterprise_sonic.sonic_mclag: + config: + domain_id: 1 + source_address: 3.3.3.3 + keepalive: 10 + session_timeout: 30 + unique_ip: + vlans: + - vlan: Vlan5 + members: + portchannels: + - lag: PortChannel12 + state: merged +# +# After state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : 3.3.3.3 +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 10 secs +# Session Timeout : 30 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:2 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# PortChannel12 down/down +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# }, +# "Vlan5": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +# +# +# Using deleted +# +# Before state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : 3.3.3.3 +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 10 secs +# Session Timeout : 30 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:1 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +# +- name: Delete device configuration based on the provided configuration + dellemc.enterprise_sonic.sonic_mclag: + config: + domain_id: 1 + source_address: 3.3.3.3 + keepalive: 10 + members: + portchannels: + - lag: PortChannel10 + state: deleted +# +# After state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 1 secs +# Session Timeout : 15 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:0 +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +# +# +# +# Using deleted +# +# Before state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : 3.3.3.3 +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 10 secs +# Session Timeout : 30 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:1 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +# +- name: Delete all device configuration + dellemc.enterprise_sonic.sonic_mclag: + config: + state: deleted +# +# After state: +# ------------ +# +# sonic# show mclag brief +# MCLAG Not Configured +# +# admin@sonic:~$ show runningconfiguration all | grep MCLAG_UNIQUE_IP +# admin@sonic:~$ +# +# +# Using deleted +# +# Before state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : 3.3.3.3 +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 10 secs +# Session Timeout : 30 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:2 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# PortChannel12 down/sown +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +- name: Delete device configuration based on the provided configuration + dellemc.enterprise_sonic.sonic_mclag: + config: + domain_id: 1 + source_address: 3.3.3.3 + keepalive: 10 + members: + portchannels: + - lag: PortChannel10 + state: deleted +# +# After state: +# ------------ +# +# sonic# show mclag brief +# +# Domain ID : 1 +# Role : standby +# Session Status : down +# Peer Link Status : down +# Source Address : +# Peer Address : 1.1.1.1 +# Peer Link : PortChannel1 +# Keepalive Interval : 1 secs +# Session Timeout : 15 secs +# System Mac : 20:04:0f:37:bd:c9 +# +# +# Number of MLAG Interfaces:0 +# +# admin@sonic:~$ show runningconfiguration all +# { +# ... +# "MCLAG_UNIQUE_IP": { +# "Vlan4": { +# "unique_ip": "enable" +# } +# }, +# ... +# } +# +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned always in the same format + of 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.mclag.mclag import MclagArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.mclag.mclag import Mclag + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=MclagArgs.argument_spec, + supports_check_mode=True) + + result = Mclag(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ntp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ntp.py new file mode 100644 index 000000000..87db8bb06 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ntp.py @@ -0,0 +1,360 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2022 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_ntp +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_ntp +version_added: 2.0.0 +short_description: Manage NTP configuration on SONiC. +description: + - This module provides configuration management of NTP for devices running SONiC. +author: "M. Zhang (@mingjunzhang2019)" +options: + config: + description: + - Specifies NTP related configurations. + type: dict + suboptions: + source_interfaces: + type: list + elements: str + description: + - List of names of NTP source interfaces. + enable_ntp_auth: + type: bool + description: + - Enable or disable NTP authentication. + trusted_keys: + type: list + elements: int + description: + - List of trusted NTP authentication keys. + vrf: + type: str + description: + - VRF name on which NTP is enabled. + servers: + type: list + elements: dict + description: + - List of NTP servers. + suboptions: + address: + type: str + description: + - IPv4/IPv6 address or host name of NTP server. + required: true + key_id: + type: int + description: + - NTP authentication key used by server. + - Key_id can not be deleted. + minpoll: + type: int + description: + - Minimum poll interval to poll NTP server. + - minpoll can not be deleted. + maxpoll: + type: int + description: + - Maximum poll interval to poll NTP server. + - maxpoll can not be deleted. + ntp_keys: + type: list + elements: dict + description: + - List of NTP authentication keys. + suboptions: + key_id: + type: int + description: + - NTP authentication key identifier. + required: true + key_type: + type: str + description: + - NTP authentication key type. + - key_type can not be deleted. + - When "state" is "merged", "key_type" is required. + choices: + - NTP_AUTH_SHA1 + - NTP_AUTH_MD5 + - NTP_AUTH_SHA2_256 + key_value: + type: str + description: + - NTP authentication key value. + - key_value can not be deleted. + - When "state" is "merged", "key_value" is required. + encrypted: + type: bool + description: + - NTP authentication key_value is encrypted. + - encrypted can not be deleted. + - When "state" is "merged", "encrypted" is required. + + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show ntp server +#---------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Authentication key ID +#---------------------------------------------------------------------- +#10.11.0.1 6 10 +#10.11.0.2 5 9 +#dell.com 6 9 +#dell.org 7 10 +# +- name: Delete NTP server configuration + ntp: + config: + servers: + - address: 10.11.0.2 + - address: dell.org + state: deleted + +# After state: +# ------------ +# +#sonic# show ntp server +#---------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Authentication key ID +#---------------------------------------------------------------------- +#10.11.0.1 6 10 +#dell.com 6 9 +# +# +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show ntp global +#---------------------------------------------- +#NTP Global Configuration +#---------------------------------------------- +#NTP source-interfaces: Ethernet0, Ethernet4, Ethernet8, Ethernet16 +# +- name: Delete NTP source-interface configuration + ntp: + config: + source_interfaces: + - Ethernet8 + - Ethernet16 + state: deleted + +# After state: +# ------------ +# +#sonic# show ntp global +#---------------------------------------------- +#NTP Global Configuration +#---------------------------------------------- +#NTP source-interfaces: Ethernet0, Ethernet4 +# +# +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show running-configuration | grep ntp +#ntp authentication-key 8 sha1 U2FsdGVkX1/NpJrdOeyMeUHEkSohY6azY9VwbAqXRTY= encrypted +#ntp authentication-key 10 md5 U2FsdGVkX1/Gxds/5pscCvIKbVngGaKka4SQineS51Y= encrypted +#ntp authentication-key 20 sha2-256 U2FsdGVkX1/eAzKj1teKhYWD7tnzOsYOijGeFAT0rKM= encrypted +# +- name: Delete NTP key configuration + ntp: + config: + ntp_keys: + - key_id: 10 + - key_id: 20 + state: deleted +# +# After state: +# ------------ +# +#sonic# show running-configuration | grep ntp +#ntp authentication-key 8 sha1 U2FsdGVkX1/NpJrdOeyMeUHEkSohY6azY9VwbAqXRTY= encrypted +# +# +# Using merged +# +# Before state: +# ------------- +# +#sonic# show ntp server +#---------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Authentication key ID +#---------------------------------------------------------------------- +#10.11.0.1 6 10 +#dell.com 6 9 +# +- name: Merge NTP server configuration + ntp: + config: + servers: + - address: 10.11.0.2 + minpoll: 5 + - address: dell.org + minpoll: 7 + maxpoll: 10 + state: merged + +# After state: +# ------------ +# +#sonic# show ntp server +#---------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Authentication key ID +#---------------------------------------------------------------------- +#10.11.0.1 6 10 +#10.11.0.2 5 9 +#dell.com 6 9 +#dell.org 7 10 +# +# +# Using merged +# +# Before state: +# ------------- +# +#sonic# show ntp global +#---------------------------------------------- +#NTP Global Configuration +#---------------------------------------------- +#NTP source-interfaces: Ethernet0, Ethernet4 +# +- name: Merge NTP source-interface configuration + ntp: + config: + source_interfaces: + - Ethernet8 + - Ethernet16 + state: merged + +# After state: +# ------------ +# +#sonic# show ntp global +#---------------------------------------------- +#NTP Global Configuration +#---------------------------------------------- +#NTP source-interfaces: Ethernet0, Ethernet4, Ethernet8, Ethernet16 +# +# +# Using merged +# +# Before state: +# ------------- +# +#sonic# show running-configuration | grep ntp +#ntp authentication-key 8 sha1 U2FsdGVkX1/NpJrdOeyMeUHEkSohY6azY9VwbAqXRTY= encrypted +# +- name: Merge NTP key configuration + ntp: + config: + ntp_keys: + - key_id: 10 + key_type: NTP_AUTH_MD5 + key_value: dellemc10 + encrypted: false + - key_id: 20 + key_type: NTP_AUTH_SHA2_256 + key_value: dellemc20 + encrypted: false + state: merged +# +# After state: +# ------------ +# +#sonic# show running-configuration | grep ntp +#ntp authentication-key 8 sha1 U2FsdGVkX1/NpJrdOeyMeUHEkSohY6azY9VwbAqXRTY= encrypted +#ntp authentication-key 10 md5 U2FsdGVkX1/Gxds/5pscCvIKbVngGaKka4SQineS51Y= encrypted +#ntp authentication-key 20 sha2-256 U2FsdGVkX1/eAzKj1teKhYWD7tnzOsYOijGeFAT0rKM= encrypted +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.ntp.ntp import NtpArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.ntp.ntp import Ntp + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=NtpArgs.argument_spec, + supports_check_mode=True) + + result = Ntp(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_breakout.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_breakout.py new file mode 100644 index 000000000..66ea00476 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_breakout.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2020 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_port_breakout +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_port_breakout +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Niraimadaiselvam M (@niraimadaiselvamm) +short_description: Configure port breakout settings on physical interfaces +description: + - This module provides configuration management of port breakout parameters on devices running Enterprise SONiC. +options: + config: + description: + - Specifies the port breakout related configuration. + type: list + elements: dict + suboptions: + name: + description: + - Specifies the name of the port breakout. + type: str + required: true + mode: + description: + - Specifies the mode of the port breakout. + type: str + choices: + - 1x100G + - 1x400G + - 1x40G + - 2x100G + - 2x200G + - 2x50G + - 4x100G + - 4x10G + - 4x25G + - 4x50G + state: + description: + - Specifies the operation to be performed on the port breakout configured on the device. + - In case of merged, the input mode configuration will be merged with the existing port breakout configuration on the device. + - In case of deleted the existing port breakout mode configuration will be removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#do show interface breakout +#----------------------------------------------- +#Port Breakout Mode Status Interfaces +#----------------------------------------------- +#1/1 4x10G Completed Eth1/1/1 +# Eth1/1/2 +# Eth1/1/3 +# Eth1/1/4 +#1/11 1x100G Completed Eth1/11 +# + +- name: Merge users configurations + dellemc.enterprise_sonic.sonic_port_breakout: + config: + - name: 1/11 + mode: 1x100G + state: deleted + +# After state: +# ------------ +# +#do show interface breakout +#----------------------------------------------- +#Port Breakout Mode Status Interfaces +#----------------------------------------------- +#1/1 4x10G Completed Eth1/1/1 +# Eth1/1/2 +# Eth1/1/3 +# Eth1/1/4 +#1/11 Default Completed Ethernet40 + + +# Using deleted +# +# Before state: +# ------------- +# +#do show interface breakout +#----------------------------------------------- +#Port Breakout Mode Status Interfaces +#----------------------------------------------- +#1/1 4x10G Completed Eth1/1/1 +# Eth1/1/2 +# Eth1/1/3 +# Eth1/1/4 +#1/11 1x100G Completed Eth1/11 +# +- name: Merge users configurations + dellemc.enterprise_sonic.sonic_port_breakout: + config: + state: deleted + + +# After state: +# ------------ +# +#do show interface breakout +#----------------------------------------------- +#Port Breakout Mode Status Interfaces +#----------------------------------------------- +#1/1 Default Completed Ethernet0 +#1/11 Default Completed Ethernet40 + + +# Using merged +# +# Before state: +# ------------- +# +#do show interface breakout +#----------------------------------------------- +#Port Breakout Mode Status Interfaces +#----------------------------------------------- +#1/1 4x10G Completed Eth1/1/1 +# Eth1/1/2 +# Eth1/1/3 +# Eth1/1/4 +# +- name: Merge users configurations + dellemc.enterprise_sonic.sonic_port_breakout: + config: + - name: 1/11 + mode: 1x100G + state: merged + + +# After state: +# ------------ +# +#do show interface breakout +#----------------------------------------------- +#Port Breakout Mode Status Interfaces +#----------------------------------------------- +#1/1 4x10G Completed Eth1/1/1 +# Eth1/1/2 +# Eth1/1/3 +# Eth1/1/4 +#1/11 1x100G Completed Eth1/11 + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.port_breakout.port_breakout import Port_breakoutArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.port_breakout.port_breakout import Port_breakout + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Port_breakoutArgs.argument_spec, + supports_check_mode=True) + + result = Port_breakout(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_prefix_lists.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_prefix_lists.py new file mode 100644 index 000000000..5a734e8b2 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_prefix_lists.py @@ -0,0 +1,423 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_prefix_lists +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_prefix_lists +version_added: "2.0.0" +author: Kerry Meyer (@kerry-meyer) +short_description: prefix list configuration handling for SONiC +description: + - This module provides configuration management for prefix list parameters on devices running SONiC. +options: + config: + description: + - Specifies a list of prefix set configuration dictionaries + type: list + elements: dict + suboptions: + name: + description: + - Name of a prefix set (a list of prefix entries) + type: str + required: true + afi: + description: + - Specifies the Address Family for addresses in the prefix list entries + type: str + choices: ["ipv4", "ipv6"] + default: "ipv4" + prefixes: + description: + - A list of prefix entries + type: list + elements: dict + suboptions: + sequence: + description: + - Precedence for this prefix entry (unique within the prefix list) + type: int + required: true + action: + description: + - Action to be taken for addresses matching this prefix entry + type: str + required: true + choices: ["permit", "deny"] + prefix: + description: + - IPv4 or IPv6 prefix in A.B.C.D/LEN or A:B::C:D/LEN format + type: str + required: true + ge: + description: Minimum prefix length to be matched + type: int + le: + description: Maximum prefix length to be matched + type: int + state: + description: + - Specifies the type of configuration update to be performed on the device. + - For "merged", merge specified attributes with existing configured attributes. + - For "deleted", delete the specified attributes from exiting configuration. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using "merged" state to create initial configuration +# +# Before state: +# ------------- +# +# sonic# show running-configuration ip prefix-list +# sonic# +# (No configuration present) +# +# ------------- +# +- name: Merge initial prefix-list configuration + dellemc.enterprise_sonic.sonic_prefix_lists: + config: + - name: pfx1 + afi: "ipv4" + prefixes: + - sequence: 10 + prefix: "1.2.3.4/24" + action: "permit" + ge: 26 + le: 30 + state: merged + +# After state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx1 seq 10 permit 1.2.3.4/24 ge 26 le 30 +# ------------ +# +# *************************************************************** +# Using "merged" state to update and add configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx1 seq 10 permit 1.2.3.4/24 ge 26 le 30 +# +# sonic# show running-configuration ipv6 prefix-list +# sonic# +# (no IPv6 prefix-list configuration present) +# +# ------------ +# +- name: Merge additional prefix-list configuration + dellemc.enterprise_sonic.sonic_prefix_lists: + config: + - name: pfx1 + afi: "ipv4" + prefixes: + - sequence: 20 + action: "deny" + prefix: "1.2.3.12/26" + - sequence: 30 + action: "permit" + prefix: "7.8.9.0/24" + - name: pfx6 + afi: "ipv6" + prefixes: + - sequence: 25 + action: "permit" + prefix: "40::300/124" + state: merged + +# After state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx1 seq 10 permit 1.2.3.4/24 ge 26 le 30 +# ip prefix-list pfx1 seq 20 deny 1.2.3.12/26 +# ip prefix-list pfx1 seq 30 permit 7.8.9.0/24 +# +# sonic# show running-configuration ipv6 prefix-list +# ! +# ipv6 prefix-list pfx6 seq 25 permit 40::300/124 +# +# *************************************************************** +# Using "deleted" state to remove configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx1 seq 10 permit 1.2.3.4/24 ge 26 le 30 +# ip prefix-list pfx1 seq 20 deny 1.2.3.12/26 +# ip prefix-list pfx1 seq 30 permit 7.8.9.0/24 +# +# sonic# show running-configuration ipv6 prefix-list +# ! +# ipv6 prefix-list pfx6 seq 25 permit 40::300/124 +# +# ------------ +# +- name: Delete selected prefix-list configuration + dellemc.enterprise_sonic.sonic_prefix_lists: + config: + - name: pfx1 + afi: "ipv4" + prefixes: + - sequence: 10 + prefix: "1.2.3.4/24" + action: "permit" + ge: 26 + le: 30 + - sequence: 20 + action: "deny" + prefix: "1.2.3.12/26" + - name: pfx6 + afi: "ipv6" + prefixes: + - sequence: 25 + action: "permit" + prefix: "40::300/124" + state: deleted + +# After state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx1 seq 30 permit 7.8.9.0/24 +# +# sonic# show running-configuration ipv6 prefix-list +# sonic# +# (no IPv6 prefix-list configuration present) +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + +# "before": [ +# { +# "afi": "ipv6", +# "name": "pf4", +# "prefixes": [ +# { +# "action": "permit", +# "ge": null, +# "le": null, +# "prefix": "50:60::/64", +# "sequence": 40 +# } +# ] +# }, +# { +# "afi": "ipv4", +# "name": "pf3", +# "prefixes": [ +# { +# "action": "deny", +# "ge": null, +# "le": 27, +# "prefix": "1.2.3.128/25", +# "sequence": 30 +# } +# ] +# }, +# { +# "afi": "ipv4", +# "name": "pf2", +# "prefixes": [ +# { +# "action": "permit", +# "ge": 27, +# "le": 29, +# "prefix": "10.20.30.128/25", +# "sequence": 50 +# }, +# { +# "action": "deny", +# "ge": 26, +# "le": null, +# "prefix": "10.20.30.0/24", +# "sequence": 20 +# } +# ] +# }, +# { +# "afi": "ipv4", +# "name": "pf1", +# "prefixes": [ +# { +# "action": "deny", +# "ge": 25, +# "le": 27, +# "prefix": "1.2.3.0/24", +# "sequence": 10 +# } +# ] +# } +# ] + +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + +# "after": [ +# { +# "afi": "ipv4", +# "name": "pf5", +# "prefixes": [ +# { +# "action": "permit", +# "ge": null, +# "le": null, +# "prefix": "15.25.35.0/24", +# "sequence": 15 +# } +# ] +# }, +# { +# "afi": "ipv4", +# "name": "pf1", +# "prefixes": [ +# { +# "action": "deny", +# "ge": 25, +# "le": 27, +# "prefix": "1.2.3.0/24", +# "sequence": 10 +# } +# ] +# }, +# { +# "afi": "ipv6", +# "name": "pf4", +# "prefixes": [ +# { +# "action": "permit", +# "ge": null, +# "le": null, +# "prefix": "50:60::/64", +# "sequence": 40 +# } +# ] +# }, +# { +# "afi": "ipv4", +# "name": "pf3", +# "prefixes": [ +# { +# "action": "deny", +# "ge": null, +# "le": 27, +# "prefix": "1.2.3.128/25", +# "sequence": 30 +# } +# ] +# }, +# { +# "afi": "ipv4", +# "name": "pf2", +# "prefixes": [ +# { +# "action": "permit", +# "ge": 27, +# "le": 29, +# "prefix": "10.20.30.128/25", +# "sequence": 50 +# }, +# { +# "action": "deny", +# "ge": 26, +# "le": null, +# "prefix": "10.20.30.0/24", +# "sequence": 20 +# } +# ] +# } +# ] + +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + +# "commands": [ +# { +# "afi": "ipv4", +# "name": "pf5", +# "prefixes": [ +# { +# "action": "permit", +# "prefix": "15.25.35.0/24", +# "sequence": 15 +# } +# ], +# "state": "merged" +# } +# ], +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.prefix_lists.prefix_lists import Prefix_listsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.prefix_lists.prefix_lists import Prefix_lists + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Prefix_listsArgs.argument_spec, + supports_check_mode=True) + + result = Prefix_lists(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_radius_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_radius_server.py new file mode 100644 index 000000000..1df4aff61 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_radius_server.py @@ -0,0 +1,328 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2021 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_radius_server +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_radius_server +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Niraimadaiselvam M (@niraimadaiselvamm) +short_description: Manage RADIUS server and its parameters +description: + - This module provides configuration management of radius server parameters on devices running Enterprise SONiC. +options: + config: + description: + - Specifies the radius server related configuration. + type: dict + suboptions: + auth_type: + description: + - Specifies the authentication type of the radius server. + type: str + choices: + - pap + - chap + - mschapv2 + default: pap + key: + description: + - Specifies the key of the radius server. + type: str + nas_ip: + description: + - Specifies the network access server of the radius server. + type: str + statistics: + description: + - Specifies the statistics flag of the radius server. + type: bool + timeout: + description: + - Specifies the timeout of the radius server. + type: int + retransmit: + description: + - Specifies the re-transmit value of the radius server. + type: int + servers: + description: + - Specifies the servers list of the radius server. + type: dict + suboptions: + host: + description: + - Specifies the host details of the radius servers list. + type: list + elements: dict + suboptions: + name: + description: + - Specifies the name of the radius server host. + type: str + auth_type: + description: + - Specifies the authentication type of the radius server host. + type: str + choices: + - pap + - chap + - mschapv2 + key: + description: + - Specifies the key of the radius server host. + type: str + priority: + description: + - Specifies the priority of the radius server host. + type: int + port: + description: + - Specifies the port of the radius server host. + type: int + timeout: + description: + - Specifies the timeout of the radius server host. + type: int + retransmit: + description: + - Specifies the retransmit of the radius server host. + type: int + source_interface: + description: + - Specifies the source interface of the radius server host. + type: str + vrf: + description: + - Specifies the vrf of the radius server host. + type: str + state: + description: + - Specifies the operation to be performed on the radius server configured on the device. + - In case of merged, the input mode configuration will be merged with the existing radius server configuration on the device. + - In case of deleted the existing radius server mode configuration will be removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#nas-ip-addr: 1.2.3.4 +#statistics : True +#timeout : 10 +#auth-type : chap +#key : chap +#retransmit : 3 +#-------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------- +#localhost mschapv2 local 52 2 20 2 mgmt Ethernet12 +#myhost chap local 53 3 23 3 mgmt Ethernet24 +#--------------------------------------------------------- +#RADIUS Statistics +#--------------------------------------------------------- +# + +- name: Merge radius configurations + dellemc.enterprise_sonic.sonic_radius_server: + config: + auth_type: chap + nas_ip: 1.2.3.4 + statistics: true + timeout: 10 + servers: + host: + - name: localhost + state: deleted + +# After state: +# ------------ +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#timeout : 5 +#auth-type : pap +#key : chap +#retransmit : 3 +#-------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------- +#myhost chap local 53 3 23 3 mgmt Ethernet24 + + +# Using deleted +# +# Before state: +# ------------- +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#nas-ip-addr: 1.2.3.4 +#statistics : True +#timeout : 10 +#auth-type : chap +#key : chap +#retransmit : 3 +#-------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------- +#localhost mschapv2 local 52 2 20 2 mgmt Ethernet12 +#myhost chap local 53 3 23 3 mgmt Ethernet24 +#--------------------------------------------------------- +#RADIUS Statistics +#--------------------------------------------------------- +# +- name: Merge radius configurations + dellemc.enterprise_sonic.sonic_radius_server: + config: + state: deleted + +# After state: +# ------------ +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#timeout : 5 +#auth-type : pap + + +# Using merged +# +# Before state: +# ------------- +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +# +- name: Merge radius configurations + dellemc.enterprise_sonic.sonic_radius_server: + config: + auth_type: chap + key: chap + nas_ip: 1.2.3.4 + statistics: true + timeout: 10 + retransmit: 3 + servers: + host: + - name: localhost + auth_type: mschapv2 + key: local + priority: 2 + port: 52 + retransmit: 2 + timeout: 20 + source_interface: Eth 12 + vrf: mgmt + state: merged + +# After state: +# ------------ +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#nas-ip-addr: 1.2.3.4 +#statistics : True +#timeout : 10 +#auth-type : chap +#key : chap +#retransmit : 3 +#-------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------- +#localhost mschapv2 local 52 2 20 2 mgmt Ethernet12 +#--------------------------------------------------------- +#RADIUS Statistics +#--------------------------------------------------------- + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.radius_server.radius_server import Radius_serverArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.radius_server.radius_server import Radius_server + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Radius_serverArgs.argument_spec, + supports_check_mode=True) + + result = Radius_server(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_static_routes.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_static_routes.py new file mode 100644 index 000000000..7a528cdf0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_static_routes.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2022 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_static_routes +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_static_routes +version_added: 2.0.0 +short_description: Manage static routes configuration on SONiC +description: + - This module provides configuration management of static routes for devices running SONiC +author: "Shade Talabi (@stalabi1)" +options: + config: + type: list + elements: dict + description: + - Manages 'static_routes' configurations + suboptions: + vrf_name: + required: True + type: str + description: + - Name of the configured VRF on the device. + static_list: + type: list + elements: dict + description: + - A list of 'static_routes' configurations. + suboptions: + prefix: + required: True + type: str + description: + - Destination prefix for the static route, either IPv4 or IPv6. + next_hops: + type: list + elements: dict + description: + - A list of next-hops to be utilised for the static route being specified. + suboptions: + index: + required: True + type: dict + description: + - An identifier utilised to uniquely reference the next-hop. + suboptions: + blackhole: + type: bool + default: False + description: + - Indicates that packets matching this route should be discarded. + interface: + type: str + description: + - The reference to a base interface. + nexthop_vrf: + type: str + description: + - Name of the next-hop network instance for leaked routes. + next_hop: + type: str + description: + - The next-hop that is to be used for the static route. + metric: + type: int + description: + - Specifies the preference of the next-hop entry when it is injected into the RIB. + track: + type: int + description: + - The IP SLA track ID for static route. + tag: + type: int + description: + - The tag value for the static route. + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ + +# Using merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip route" +# (No "ip route" configuration present) + + - name: Merge static routes configurations + dellemc.enterprise_sonic.sonic_static_routes: + config: + - vrf_name: 'default' + static_list: + - prefix: '2.0.0.0/8' + next_hops: + - index: + interface: 'Ethernet4' + metric: 1 + tag: 2 + track: 3 + - index: + next_hop: '3.0.0.0' + metric: 2 + tag: 4 + track: 8 + - vrf_name: '{{vrf_1}}' + static_list: + - prefix: '3.0.0.0/8' + next_hops: + - index: + interface: 'eth0' + nexthop_vrf: '{{vrf_2}}' + next_hop: '4.0.0.0' + metric: 4 + tag: 5 + track: 6 + - index: + blackhole: True + metric: 10 + tag: 20 + track: 30 + state: merged + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip route" +# ip route 2.0.0.0/8 3.0.0.0 tag 4 track 8 2 +# ip route 2.0.0.0/8 interface Ethernet4 tag 2 track 3 1 +# ip route vrf VrfReg1 3.0.0.0/8 4.0.0.0 interface Management 0 nexthop-vrf VrfReg2 tag 5 track 6 4 +# ip route vrf VrfREg1 3.0.0.0/8 blackhole tag 20 track 30 10 +# +# +# Modifying previous merge + + - name: Modify static routes configurations + dellemc.enterprise_sonic.sonic_static_routes: + config: + - vrf_name: '{{vrf_1}}' + static_list: + - prefix: '3.0.0.0/8' + next_hops: + - index: + blackhole: True + metric: 11 + tag: 22 + track: 33 + state: merged + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip route" +# ip route 2.0.0.0/8 3.0.0.0 tag 4 track 8 2 +# ip route 2.0.0.0/8 interface Ethernet4 tag 2 track 3 1 +# ip route vrf VrfReg1 3.0.0.0/8 4.0.0.0 interface Management 0 nexthop-vrf VrfReg2 tag 5 track 6 4 +# ip route vrf VrfREg1 3.0.0.0/8 blackhole tag 22 track 33 11 + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip route" +# ip route 2.0.0.0/8 3.0.0.0 tag 4 track 8 2 +# ip route 2.0.0.0/8 interface Ethernet4 tag 2 track 3 1 +# ip route vrf VrfReg1 3.0.0.0/8 4.0.0.0 interface Management 0 nexthop-vrf VrfReg2 tag 5 track 6 4 +# ip route vrf VrfREg1 3.0.0.0/8 blackhole tag 22 track 33 11 + + - name: Delete static routes configurations + dellemc.enterprise_sonic.sonic_static_routes: + config: + - vrf_name: 'default' + static_list: + - prefix: '2.0.0.0/8' + next_hops: + - index: + interface: 'Ethernet4' + - vrf_name: '{{vrf_1}}' + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip route" +# ip route 2.0.0.0/8 3.0.0.0 tag 4 track 8 2 + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.static_routes.static_routes import Static_routesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.static_routes.static_routes import Static_routes + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Static_routesArgs.argument_spec, + supports_check_mode=True) + + result = Static_routes(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_system.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_system.py new file mode 100644 index 000000000..efb285a11 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_system.py @@ -0,0 +1,214 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 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_system +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_system +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Abirami N (@abirami-n) +short_description: Configure system parameters +description: + - This module is used for configuration management of global system parameters on devices running Enterprise SONiC. +options: + config: + description: + - Specifies the system related configurations + type: dict + suboptions: + hostname: + description: + - Specifies the hostname of the SONiC device + type: str + interface_naming: + description: + - Specifies the type of interface-naming in device + type: str + choices: + - standard + - native + anycast_address: + description: + - Specifies different types of anycast address that can be configured on the device + type: dict + suboptions: + ipv4: + description: + - Enable or disable ipv4 anycast-address + type: bool + ipv6: + description: + - Enable or disable ipv6 anycast-address + type: bool + mac_address: + description: + - Specifies the mac anycast-address + type: str + state: + description: + - Specifies the operation to be performed on the system parameters configured on the device. + - In case of merged, the input configuration will be merged with the existing system configuration on the device. + - In case of deleted the existing system configuration will be removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +#! +#SONIC(config)#do show running-configuration +#! +#ip anycast-mac-address aa:bb:cc:dd:ee:ff +#ip anycast-address enable +#ipv6 anycast-address enable +#interface-naming standard + +- name: Merge provided configuration with device configuration + dellemc.enterprise_sonic.sonic_system: + config: + hostname: SONIC + interface_naming: standard + anycast_address: + ipv6: true + state: deleted + +# After state: +# ------------ +#! +#sonic(config)#do show running-configuration +#! +#ip anycast-mac-address aa:bb:cc:dd:ee:ff +#ip anycast-address enable + + +# Using deleted +# +# Before state: +# ------------- +#! +#SONIC(config)#do show running-configuration +#! +#ip anycast-mac-address aa:bb:cc:dd:ee:ff +#ip anycast-address enable +#ipv6 anycast-address enable +#interface-naming standard + +- name: Delete all system related configs in device configuration + dellemc.enterprise_sonic.sonic_system: + config: + state: deleted + +# After state: +# ------------ +#! +#sonic(config)#do show running-configuration +#! + + +# Using merged +# +# Before state: +# ------------- +#! +#sonic(config)#do show running-configuration +#! + +- name: Merge provided configuration with device configuration + dellemc.enterprise_sonic.sonic_system: + config: + hostname: SONIC + interface_naming: standard + anycast_address: + ipv6: true + ipv4: true + mac_address: aa:bb:cc:dd:ee:ff + state: merged + +# After state: +# ------------ +#! +#SONIC(config)#do show running-configuration +#! +#ip anycast-mac-address aa:bb:cc:dd:ee:ff +#ip anycast-address enable +#ipv6 anycast-address enable +#interface-naming standard + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.system.system import SystemArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.system.system import System + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=SystemArgs.argument_spec, + supports_check_mode=True) + + result = System(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_tacacs_server.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_tacacs_server.py new file mode 100644 index 000000000..3295e11ba --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_tacacs_server.py @@ -0,0 +1,297 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2021 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_tacacs_server +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_tacacs_server +version_added: 1.1.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Niraimadaiselvam M (@niraimadaiselvamm) +short_description: Manage TACACS server and its parameters +description: + - This module provides configuration management of tacacs server parameters on devices running Enterprise SONiC. +options: + config: + description: + - Specifies the tacacs server related configuration. + type: dict + suboptions: + auth_type: + description: + - Specifies the authentication type of the tacacs server. + type: str + choices: + - pap + - chap + - mschap + - login + default: pap + key: + description: + - Specifies the key of the tacacs server. + type: str + timeout: + description: + - Specifies the timeout of the tacacs server. + type: int + source_interface: + description: + - Specifies the source interface of the tacacs server. + type: str + servers: + description: + - Specifies the servers list of the tacacs server. + type: dict + suboptions: + host: + description: + - Specifies the host details of the tacacs servers list. + type: list + elements: dict + suboptions: + name: + description: + - Specifies the name of the tacacs server host. + type: str + auth_type: + description: + - Specifies the authentication type of the tacacs server host. + type: str + choices: + - pap + - chap + - mschap + - login + default: pap + key: + description: + - Specifies the key of the tacacs server host. + type: str + priority: + description: + - Specifies the priority of the tacacs server host. + type: int + default: 1 + port: + description: + - Specifies the port of the tacacs server host. + type: int + default: 49 + timeout: + description: + - Specifies the timeout of the tacacs server host. + type: int + default: 5 + vrf: + description: + - Specifies the vrf of the tacacs server host. + type: str + default: default + state: + description: + - Specifies the operation to be performed on the tacacs server configured on the device. + - In case of merged, the input mode configuration will be merged with the existing tacacs server configuration on the device. + - In case of deleted the existing tacacs server mode configuration will be removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#source-interface : Ethernet12 +#timeout : 10 +#auth-type : login +#key : login +#------------------------------------------------------------------------------------------------ +#HOST AUTH-TYPE KEY PORT PRIORITY TIMEOUT VRF +#------------------------------------------------------------------------------------------------ +#1.2.3.4 pap ***** 50 2 10 mgmt +#localhost pap 49 1 5 default +# + +- name: Merge tacacs configurations + dellemc.enterprise_sonic.sonic_tacacs_server: + config: + auth_type: login + key: login + source_interface: Ethernet 12 + timeout: 10 + servers: + host: + - name: 1.2.3.4 + state: deleted + +# After state: +# ------------ +# +#do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#timeout : 5 +#auth-type : pap +#------------------------------------------------------------------------------------------------ +#HOST AUTH-TYPE KEY PORT PRIORITY TIMEOUT VRF +#------------------------------------------------------------------------------------------------ +#localhost pap 49 1 5 default + + +# Using deleted +# +# Before state: +# ------------- +# +# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#source-interface : Ethernet12 +#timeout : 10 +#auth-type : login +#key : login +#------------------------------------------------------------------------------------------------ +#HOST AUTH-TYPE KEY PORT PRIORITY TIMEOUT VRF +#------------------------------------------------------------------------------------------------ +#1.2.3.4 pap ***** 50 2 10 mgmt +#localhost pap 49 1 5 default +# + +- name: Merge tacacs configurations + dellemc.enterprise_sonic.sonic_tacacs_server: + config: + state: deleted + +# After state: +# ------------ +# +#do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#timeout : 5 +#auth-type : pap + + +# Using merged +# +# Before state: +# ------------- +# +#sonic(config)# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +# +- name: Merge tacacs configurations + dellemc.enterprise_sonic.sonic_tacacs_server: + config: + auth_type: pap + key: pap + source_interface: Ethernet 12 + timeout: 10 + servers: + host: + - name: 1.2.3.4 + auth_type: pap + key: 1234 + state: merged + +# After state: +# ------------ +# +#sonic(config)# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#source-interface : Ethernet12 +#timeout : 10 +#auth-type : pap +#key : pap +#------------------------------------------------------------------------------------------------ +#HOST AUTH-TYPE KEY PORT PRIORITY TIMEOUT VRF +#------------------------------------------------------------------------------------------------ +#1.2.3.4 pap 1234 49 1 5 default + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.tacacs_server.tacacs_server import Tacacs_serverArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.tacacs_server.tacacs_server import Tacacs_server + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Tacacs_serverArgs.argument_spec, + supports_check_mode=True) + + result = Tacacs_server(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_users.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_users.py new file mode 100644 index 000000000..7f0855a94 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_users.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# 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_users +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_users +version_added: 1.1.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Niraimadaiselvam M (@niraimadaiselvamm) +short_description: Manage users and its parameters +description: + - This module provides configuration management of users parameters on devices running Enterprise SONiC. +options: + config: + description: + - Specifies the users related configuration. + type: list + elements: dict + suboptions: + name: + description: + - Specifies the name of the user. + type: str + required: true + role: + description: + - Specifies the role of the user. + type: str + choices: + - admin + - operator + password: + description: + - Specifies the password of the user. + type: str + update_password: + description: + - Specifies the update password flag. + - In case of always, password will be updated every time. + - In case of on_create, password will be updated only when user is created. + type: str + choices: + - always + - on_create + default: always + state: + description: + - Specifies the operation to be performed on the users configured on the device. + - In case of merged, the input configuration will be merged with the existing users configuration on the device. + - In case of deleted the existing users configuration will be removed from the device. + default: merged + choices: ['merged', 'deleted'] + type: str +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#do show running-configuration +#! +#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin +#username sysadmin password $6$3QNqJzpFAPL9JqHA$417xFKw6SRn.CiqMFJkDfQJXKJGjeYwi2A8BIyfuWjGimvunOOjTRunVluudey/W9l8jhzN1oewBW5iLxmq2Q1 role admin +#username sysoperator password $6$s1eTVjcX4Udi69gY$zlYgqwoKRGC6hGL5iKDImN/4BL7LXKNsx9e5PoSsBLs6C80ShYj2LoJAUZ58ia2WNjcHXhTD1p8eU9wyRTCiE0 role operator +# +- name: Merge users configurations + dellemc.enterprise_sonic.sonic_users: + config: + - name: sysoperator + state: deleted +# After state: +# ------------ +# +#do show running-configuration +#! +#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin +#username sysadmin password $6$3QNqJzpFAPL9JqHA$417xFKw6SRn.CiqMFJkDfQJXKJGjeYwi2A8BIyfuWjGimvunOOjTRunVluudey/W9l8jhzN1oewBW5iLxmq2Q1 role admin + + +# Using deleted +# +# Before state: +# ------------- +# +#do show running-configuration +#! +#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin +#username sysadmin password $6$3QNqJzpFAPL9JqHA$417xFKw6SRn.CiqMFJkDfQJXKJGjeYwi2A8BIyfuWjGimvunOOjTRunVluudey/W9l8jhzN1oewBW5iLxmq2Q1 role admin +#username sysoperator password $6$s1eTVjcX4Udi69gY$zlYgqwoKRGC6hGL5iKDImN/4BL7LXKNsx9e5PoSsBLs6C80ShYj2LoJAUZ58ia2WNjcHXhTD1p8eU9wyRTCiE0 role operator +# +- name: Merge users configurations + dellemc.enterprise_sonic.sonic_users: + config: + state: deleted + +# After state: +# ------------ +# +#do show running-configuration +#! +#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin + + +# Using merged +# +# Before state: +# ------------- +# +#do show running-configuration +#! +#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin +# +- name: Merge users configurations + dellemc.enterprise_sonic.sonic_users: + config: + - name: sysadmin + role: admin + password: admin + update_password: always + - name: sysoperator + role: operator + password: operator + update_password: always + state: merged + +# After state: +# ------------ +#! +#do show running-configuration +#! +#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin +#username sysadmin password $6$3QNqJzpFAPL9JqHA$417xFKw6SRn.CiqMFJkDfQJXKJGjeYwi2A8BIyfuWjGimvunOOjTRunVluudey/W9l8jhzN1oewBW5iLxmq2Q1 role admin +#username sysoperator password $6$s1eTVjcX4Udi69gY$zlYgqwoKRGC6hGL5iKDImN/4BL7LXKNsx9e5PoSsBLs6C80ShYj2LoJAUZ58ia2WNjcHXhTD1p8eU9wyRTCiE0 role operator + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of 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.users.users import UsersArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.users.users import Users + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=UsersArgs.argument_spec, + supports_check_mode=True) + + result = Users(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlans.py new file mode 100644 index 000000000..cfd536c79 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlans.py @@ -0,0 +1,241 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 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_vlans +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_vlans +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +author: Mohamed Javeed (@javeedf) +short_description: Manage VLAN and its parameters +description: + - This module provides configuration management of VLANs parameters + on devices running Enterprise SONiC Distribution by Dell Technologies. +options: + config: + description: A dictionary of VLAN options. + type: list + elements: dict + suboptions: + vlan_id: + description: + - ID of the VLAN + - Range is 1 to 4094 + type: int + required: true + description: + description: + - Description about the VLAN. + type: str + state: + description: + - The state that the configuration should be left in. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#30 Inactive +# +#sonic# +# + + +- name: Merges given VLAN attributes with the device configuration + dellemc.enterprise_sonic.sonic_vlans: + config: + - vlan_id: 10 + description: "Internal" + state: merged + +# After state: +# ------------ +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#30 Inactive +# +#sonic# +# +#sonic# show interface Vlan 10 +#Description: Internal +#Vlan10 is up +#Mode of IPV4 address assignment: not-set +#Mode of IPV6 address assignment: not-set +#IP MTU 6000 bytes +#sonic# +# + + +# Using deleted + +# Before state: +# ------------- +# +#sonic# show interface Vlan 70 +#Description: Internal +#Vlan70 is up +#Mode of IPV4 address assignment: not-set +#Mode of IPV6 address assignment: not-set +#IP MTU 6000 bytes + +- name: Deletes attributes of the given VLANs + dellemc.enterprise_sonic.sonic_vlans: + config: + - vlan_id: 70 + description: "Internal" + state: deleted + +# After state: +# ------------ +# +#sonic# show interface Vlan 70 +#Vlan70 is up +#Mode of IPV4 address assignment: not-set +#Mode of IPV6 address assignment: not-set +#IP MTU 6000 bytes + +# Before state: +# ------------- +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#20 Inactive +# +#sonic# + +- name: Deletes attributes of the given VLANs + dellemc.enterprise_sonic.sonic_vlans: + config: + - vlan_id: 20 + state: deleted + +# After state: +# ------------ +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +# +#sonic# + + +# Using deleted + +# Before state: +# ------------- +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#20 Inactive +#30 Inactive +# +#sonic# + +- name: Deletes all the VLANs on the switch + dellemc.enterprise_sonic.sonic_vlans: + config: + state: deleted + +# After state: +# ------------ +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +# +#sonic# + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration that is returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.vlans.vlans import VlansArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.vlans.vlans import Vlans + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=VlansArgs.argument_spec, + supports_check_mode=True) + + result = Vlans(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vrfs.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vrfs.py new file mode 100644 index 000000000..4c881aee6 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vrfs.py @@ -0,0 +1,204 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2020 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_vrfs +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_vrfs +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage VRFs and associate VRFs to interfaces such as, Eth, LAG, VLAN, and loopback +description: Manages VRF and VRF interface attributes in Enterprise SONiC Distribution by Dell Technologies. +author: Abirami N (@abirami-n) +options: + config: + description: A list of VRF configurations. + type: list + elements: dict + suboptions: + name: + type: str + description: The name of the VRF interface. + required: true + members: + type: dict + description: Holds a dictionary mapping of list of interfaces linked to a VRF interface. + suboptions: + interfaces: + type: list + elements: dict + description: List of interface names that are linked to a specific VRF interface. + suboptions: + name: + type: str + description: The name of the physical interface. + state: + description: "The state of the configuration after module completion." + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 +#Vrfcheck2 +#Vrfcheck3 Eth1/3 +# Eth1/14 +# Eth1/16 +# Eth1/17 +#Vrfcheck4 Eth1/5 +# Eth1/6 +# +- name: Configuring vrf deleted state + dellemc.enterprise_sonic.sonic_vrfs: + config: + - name: Vrfcheck4 + members: + interfaces: + - name: Eth1/6 + - name: Vrfcheck3 + members: + interfaces: + - name: Eth1/3 + - name: Eth1/14 + state: deleted +# +# After state: +# ------------ +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 +#Vrfcheck2 +#Vrfcheck3 Eth1/16 +# Eth1/17 +#Vrfcheck4 Eth1/5 +# +# +# Using merged +# +# Before state: +# ------------- +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 +#Vrfcheck2 +#Vrfcheck3 Eth1/16 +# Eth1/17 +#Vrfcheck4 +# +- name: Configuring vrf merged state + dellemc.enterprise_sonic.sonic_vrfs: + config: + - name: Vrfcheck4 + members: + interfaces: + - name: Eth1/5 + - name: Eth1/6 + - name: Vrfcheck3 + members: + interfaces: + - name: Eth1/3 + - name: Eth1/14 + state: merged +# +# After state: +# ------------ +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 +#Vrfcheck2 +#Vrfcheck3 Eth1/3 +# Eth1/14 +# Eth1/16 +# Eth1/17 +#Vrfcheck4 Eth1/5 +# Eth1/6 +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of 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.vrfs.vrfs import VrfsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.vrfs.vrfs import Vrfs + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=VrfsArgs.argument_spec, + supports_check_mode=True) + + result = Vrfs(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vxlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vxlans.py new file mode 100644 index 000000000..e6613ba24 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vxlans.py @@ -0,0 +1,245 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2020 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_vxlans +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_vxlans +version_added: 1.0.0 +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: Manage VxLAN EVPN and its parameters +description: 'Manages interface attributes of Enterprise SONiC interfaces.' +author: Niraimadaiselvam M (@niraimadaiselvamm) +options: + config: + description: + - A list of VxLAN configurations. + - source_ip and evpn_nvo are required together. + type: list + elements: dict + suboptions: + name: + type: str + description: 'The name of the VxLAN.' + required: true + evpn_nvo: + type: str + description: 'EVPN nvo name' + source_ip: + description: 'The source IP address of the VTEP.' + type: str + primary_ip: + description: 'The vtep mclag primary ip address for this node' + type: str + vlan_map: + description: 'The list of VNI map of VLAN.' + type: list + elements: dict + suboptions: + vni: + type: int + description: 'Specifies the VNI ID.' + required: true + vlan: + type: int + description: 'VLAN ID for VNI VLAN map.' + vrf_map: + description: 'list of VNI map of VRF.' + type: list + elements: dict + suboptions: + vni: + type: int + description: 'Specifies the VNI ID.' + required: true + vrf: + type: str + description: 'VRF name for VNI VRF map.' + state: + description: 'The state of the configuration after module completion.' + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +# do show running-configuration +# +#interface vxlan vteptest1 +# source-ip 1.1.1.1 +# primary-ip 2.2.2.2 +# map vni 101 vlan 11 +# map vni 102 vlan 12 +# map vni 101 vrf Vrfcheck1 +# map vni 102 vrf Vrfcheck2 +#! +# +- name: "Test vxlans deleted state 01" + dellemc.enterprise_sonic.sonic_vxlans: + config: + - name: vteptest1 + source_ip: 1.1.1.1 + vlan_map: + - vni: 101 + vlan: 11 + vrf_map: + - vni: 101 + vrf: Vrfcheck1 + state: deleted +# +# After state: +# ------------ +# +# do show running-configuration +# +#interface vxlan vteptest1 +# source-ip 1.1.1.1 +# map vni 102 vlan 12 +# map vni 102 vrf Vrfcheck2 +#! +# +# Using deleted +# +# Before state: +# ------------- +# +# do show running-configuration +# +#interface vxlan vteptest1 +# source-ip 1.1.1.1 +# map vni 102 vlan 12 +# map vni 102 vrf Vrfcheck2 +#! +# +- name: "Test vxlans deleted state 02" + dellemc.enterprise_sonic.sonic_vxlans: + config: + state: deleted +# +# After state: +# ------------ +# +# do show running-configuration +# +#! +# +# Using merged +# +# Before state: +# ------------- +# +# do show running-configuration +# +#! +# +- name: "Test vxlans merged state 01" + dellemc.enterprise_sonic.sonic_vxlans: + config: + - name: vteptest1 + source_ip: 1.1.1.1 + primary_ip: 2.2.2.2 + evpn_nvo_name: nvo1 + vlan_map: + - vni: 101 + vlan: 11 + - vni: 102 + vlan: 12 + vrf_map: + - vni: 101 + vrf: Vrfcheck1 + - vni: 102 + vrf: Vrfcheck2 + state: merged +# +# After state: +# ------------ +# +# do show running-configuration +# +#interface vxlan vteptest1 +# source-ip 1.1.1.1 +# primary-ip 2.2.2.2 +# map vni 101 vlan 11 +# map vni 102 vlan 12 +# map vni 101 vrf Vrfcheck1 +# map vni 102 vrf Vrfcheck2 +#! +# """ +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned is always in the same format + of the parameters above. +commands: + description: The set of commands that are 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.vxlans.vxlans import VxlansArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.vxlans.vxlans import Vxlans + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=VxlansArgs.argument_spec, + supports_check_mode=True) + + result = Vxlans(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/terminal/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/terminal/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/terminal/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/terminal/sonic.py b/ansible_collections/dellemc/enterprise_sonic/plugins/terminal/sonic.py new file mode 100644 index 000000000..206657365 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/terminal/sonic.py @@ -0,0 +1,73 @@ +# +# (c) 2020 Red Hat Inc. +# +# This file is part of Ansible +# +# Copyright (c) 2020 Dell Inc. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +from ansible.errors import AnsibleConnectionFailure +from ansible.plugins.terminal import TerminalBase + +DOCUMENTATION = """ +short_description: Terminal plugin module for sonic CLI modules +version_added: 1.0.0 +""" + + +class TerminalModule(TerminalBase): + + terminal_stdout_re = [ + re.compile(br"[\r\n]?[\w+\-\.:\/\[\]]+(?:\([^\)]+\)){,3}(?:#) ?$"), + re.compile(br"\[\w+\@[\w\-\.]+(?: [^\]])\] ?[>#\$] ?$"), + re.compile(br"\$ ?$") + ] + + terminal_stderr_re = [ + re.compile(br"% ?Error"), + re.compile(br"% ?Bad secret"), + re.compile(br"Syntax error:"), + re.compile(br"invalid input", re.I), + re.compile(br"(?:incomplete|ambiguous) command", re.I), + re.compile(br"connection timed out", re.I), + re.compile(br"[^\r\n]+ not found", re.I), + re.compile(br"'[^']' +returned error code: ?\d+"), + ] + + def on_open_shell(self): + try: + if self._get_prompt().endswith(b'$ '): + self._exec_cli_command(b'sonic-cli') + self._exec_cli_command(b'terminal length 0') + except AnsibleConnectionFailure: + raise AnsibleConnectionFailure('unable to open sonic cli') + + def on_become(self, passwd=None): + if self._get_prompt().endswith(b'#'): + return + + def on_unbecome(self): + prompt = self._get_prompt() + if prompt is None: + # if prompt is None most likely the terminal is hung up at a prompt + return + + if prompt.endswith(b'#'): + self._exec_cli_command(b'exit') |