diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:35 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:35 +0000 |
commit | 7fec0b69a082aaeec72fee0612766aa42f6b1b4d (patch) | |
tree | efb569b86ca4da888717f5433e757145fa322e08 /ansible_collections/dellemc/enterprise_sonic/plugins | |
parent | Releasing progress-linux version 7.7.0+dfsg-3~progress7.99u1. (diff) | |
download | ansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.tar.xz ansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.zip |
Merging upstream version 9.4.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/dellemc/enterprise_sonic/plugins')
205 files changed, 33854 insertions, 1759 deletions
diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/sonic.py b/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/sonic.py index 37f1d872a..e5cc7630f 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/sonic.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/cliconf/sonic.py @@ -32,8 +32,6 @@ description: 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 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 index 86040892a..a61b7307b 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2021 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -60,7 +60,7 @@ class AaaArgs(object): # pylint: disable=R0903 'type': 'dict' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'overridden', 'replaced'], 'default': 'merged', 'type': 'str' } } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/acl_interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/acl_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/acl_interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/acl_interfaces/acl_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/acl_interfaces/acl_interfaces.py new file mode 100644 index 000000000..45f7bf480 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/acl_interfaces/acl_interfaces.py @@ -0,0 +1,82 @@ +# +# -*- 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_acl_interfaces module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Acl_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_acl_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'access_groups': { + 'elements': 'dict', + 'options': { + 'acls': { + 'elements': 'dict', + 'options': { + 'direction': { + 'choices': ['in', 'out'], + 'required': True, + 'type': 'str' + }, + 'name': { + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'type': { + 'choices': ['mac', 'ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'name': { + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bfd/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bfd/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bfd/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bfd/bfd.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bfd/bfd.py new file mode 100644 index 000000000..57532795e --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/bfd/bfd.py @@ -0,0 +1,89 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_bfd module +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class BfdArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_bfd module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'multi_hops': { + 'elements': 'dict', + 'options': { + 'detect_multiplier': {'default': 3, 'type': 'int'}, + 'enabled': {'default': True, 'type': 'bool'}, + 'local_address': {'required': True, 'type': 'str'}, + 'min_ttl': {'default': 254, 'type': 'int'}, + 'passive_mode': {'default': False, 'type': 'bool'}, + 'profile_name': {'type': 'str'}, + 'receive_interval': {'default': 300, 'type': 'int'}, + 'remote_address': {'required': True, 'type': 'str'}, + 'transmit_interval': {'default': 300, 'type': 'int'}, + 'vrf': {'required': True, 'type': 'str'}}, + 'type': 'list'}, + 'profiles': { + 'elements': 'dict', + 'options': { + 'detect_multiplier': {'default': 3, 'type': 'int'}, + 'echo_interval': {'default': 300, 'type': 'int'}, + 'echo_mode': {'default': False, 'type': 'bool'}, + 'enabled': {'default': True, 'type': 'bool'}, + 'min_ttl': {'default': 254, 'type': 'int'}, + 'passive_mode': {'default': False, 'type': 'bool'}, + 'profile_name': {'required': True, 'type': 'str'}, + 'receive_interval': {'default': 300, 'type': 'int'}, + 'transmit_interval': {'default': 300, 'type': 'int'}}, + 'type': 'list'}, + 'single_hops': { + 'elements': 'dict', + 'options': { + 'detect_multiplier': {'default': 3, 'type': 'int'}, + 'echo_interval': {'default': 300, 'type': 'int'}, + 'echo_mode': {'default': False, 'type': 'bool'}, + 'enabled': {'default': True, 'type': 'bool'}, + 'interface': {'required': True, 'type': 'str'}, + 'local_address': {'required': True, 'type': 'str'}, + 'passive_mode': {'default': False, 'type': 'bool'}, + 'profile_name': {'type': 'str'}, + 'receive_interval': {'default': 300, 'type': 'int'}, + 'remote_address': {'required': True, 'type': 'str'}, + 'transmit_interval': {'default': 300, 'type': 'int'}, + 'vrf': {'required': True, 'type': 'str'}}, + 'type': 'list'} + }, + 'type': 'dict' + }, + 'state': {'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str'} + } # pylint: disable=C0301 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 index fb7618133..8d494dddd 100644 --- 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 @@ -79,6 +79,7 @@ class BgpArgs(object): # pylint: disable=R0903 }, "type": "dict" }, + 'rt_delay': {'type': 'int'}, 'timers': { 'options': { 'holdtime': {'type': 'int'}, @@ -91,7 +92,7 @@ class BgpArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged' } } # pylint: disable=C0301 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 index ac22210ee..336d49b47 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2019 Red Hat +# Copyright 2023 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) @@ -71,6 +71,21 @@ class Bgp_afArgs(object): # pylint: disable=R0903 'required': True, 'type': 'str' }, + 'rd': {'type': 'str'}, + 'rt_in': {'type': 'list', 'elements': 'str'}, + 'rt_out': {'type': 'list', 'elements': 'str'}, + 'vnis': { + 'elements': 'dict', + 'options': { + 'advertise_default_gw': {'type': 'bool'}, + 'advertise_svi_ip': {'type': 'bool'}, + 'rd': {'type': 'str'}, + 'rt_in': {'type': 'list', 'elements': 'str'}, + 'rt_out': {'type': 'list', 'elements': 'str'}, + 'vni_number': {'required': True, 'type': 'int'} + }, + 'type': 'list' + }, 'max_path': { 'options': { 'ebgp': {'type': 'int'}, @@ -111,7 +126,7 @@ class Bgp_afArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'overridden', 'replaced'], 'default': 'merged' } } # pylint: disable=C0301 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 index dec9b930e..d9d4ed766 100644 --- 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 @@ -43,6 +43,6 @@ class Bgp_as_pathsArgs(object): # pylint: disable=R0903 'type': 'list'}, 'name': {'required': True, 'type': 'str'}}, 'type': 'list'}, - 'state': {'choices': ['merged', 'deleted'], + 'state': {'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str'}} # pylint: disable=C0301 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 index 867e55204..c90fab8e9 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -54,6 +54,6 @@ class Bgp_communitiesArgs(object): # pylint: disable=R0903 'default': 'standard', 'type': 'str'}}, 'type': 'list'}, - 'state': {'choices': ['merged', 'deleted'], + 'state': {'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str'}} # pylint: disable=C0301 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 index aec0f364a..4cee6182b 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -68,7 +68,7 @@ class Bgp_ext_communitiesArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str' } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/copp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/copp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/copp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/copp/copp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/copp/copp.py new file mode 100644 index 000000000..889c614c6 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/copp/copp.py @@ -0,0 +1,59 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_copp module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class CoppArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_copp module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'copp_groups': { + 'elements': 'dict', + 'options': { + 'cbs': {'type': 'str'}, + 'cir': {'type': 'str'}, + 'copp_name': {'required': True, 'type': 'str'}, + 'queue': {'type': 'int'}, + 'trap_action': {'type': 'str'}, + 'trap_priority': {'type': 'int'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'state': {'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str'} + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_relay/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_relay/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_relay/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_relay/dhcp_relay.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_relay/dhcp_relay.py new file mode 100644 index 000000000..0ca834487 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_relay/dhcp_relay.py @@ -0,0 +1,94 @@ +# +# -*- 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_dhcp_relay module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Dhcp_relayArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_dhcp_relay module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'ipv4': { + 'options': { + 'circuit_id': { + 'choices': ['%h:%p', '%i', '%p'], + 'type': 'str' + }, + 'link_select': {'type': 'bool'}, + 'max_hop_count': {'type': 'int'}, + 'policy_action': { + 'choices': ['append', 'discard', 'replace'], + 'type': 'str' + }, + 'server_addresses': { + 'elements': 'dict', + 'options': { + 'address': {'type': 'str'} + }, + 'type': 'list' + }, + 'source_interface': {'type': 'str'}, + 'vrf_name': {'type': 'str'}, + 'vrf_select': {'type': 'bool'} + }, + 'type': 'dict' + }, + 'ipv6': { + 'options': { + 'max_hop_count': {'type': 'int'}, + 'server_addresses': { + 'elements': 'dict', + 'options': { + 'address': {'type': 'str'} + }, + 'type': 'list' + }, + 'source_interface': {'type': 'str'}, + 'vrf_name': {'type': 'str'}, + 'vrf_select': {'type': 'bool'} + }, + 'type': 'dict' + }, + 'name': {'required': True, 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_snooping/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_snooping/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_snooping/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_snooping/dhcp_snooping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_snooping/dhcp_snooping.py new file mode 100644 index 000000000..6cf2ecacd --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/dhcp_snooping/dhcp_snooping.py @@ -0,0 +1,81 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_dhcp_snooping module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Dhcp_snoopingArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_dhcp_snooping module""" + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'afis': { + 'elements': 'dict', + 'options': { + 'afi': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str', + }, + 'enabled': {'type': 'bool'}, + 'source_bindings': { + 'elements': 'dict', + 'options': { + 'mac_addr': {'required': True, 'type': 'str'}, + 'ip_addr': {'type': 'str'}, + 'intf_name': {'type': 'str'}, + 'vlan_id': {'type': 'int'}, + }, + 'type': 'list', + }, + 'trusted': { + 'elements': 'dict', + 'options': { + 'intf_name': {'required': True, 'type': 'str'}, + }, + 'type': 'list', + }, + 'verify_mac': {'type': 'bool'}, + 'vlans': {'elements': 'str', 'type': 'list'}, + }, + 'type': 'list', + } + }, + 'type': 'dict', + }, + 'state': { + 'choices': ['merged', 'deleted', 'overridden', 'replaced'], + 'default': 'merged', + 'type': 'str', + }, + } # pylint: disable=C0301 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 index 3a4d02989..6b27194eb 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2021 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -35,6 +35,7 @@ class FactsArgs(object): # pylint: disable=R0903 'bgp_ext_communities', 'mclag', 'prefix_lists', + 'vlan_mapping', 'vrfs', 'vxlans', 'users', @@ -44,7 +45,22 @@ class FactsArgs(object): # pylint: disable=R0903 'tacacs_server', 'radius_server', 'static_routes', - 'ntp' + 'ntp', + 'logging', + 'pki', + 'ip_neighbor', + 'port_group', + 'dhcp_relay', + 'dhcp_snooping', + 'acl_interfaces', + 'l2_acls', + 'l3_acls', + 'lldp_global', + 'mac', + 'bfd', + 'copp', + 'route_maps', + 'stp' ] argument_spec = { 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 index 76c36a90b..407dbde6b 100644 --- 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 @@ -44,12 +44,33 @@ class InterfacesArgs(object): # pylint: disable=R0903 "description": {"type": "str"}, "enabled": {"type": "bool"}, "mtu": {"type": "int"}, - "name": {"required": True, "type": "str"} + "name": {"required": True, "type": "str"}, + "speed": {"type": "str", + "choices": ["SPEED_10MB", + "SPEED_100MB", + "SPEED_1GB", + "SPEED_2500MB", + "SPEED_5GB", + "SPEED_10GB", + "SPEED_20GB", + "SPEED_25GB", + "SPEED_40GB", + "SPEED_50GB", + "SPEED_100GB", + "SPEED_400GB"]}, + "auto_negotiate": {"type": "bool"}, + "advertised_speed": {"type": "list", "elements": "str"}, + "fec": {"type": "str", + "choices": ["FEC_RS", + "FEC_FC", + "FEC_DISABLED", + "FEC_DEFAULT", + "FEC_AUTO"]} }, "type": "list" }, "state": { - "choices": ["merged", "deleted"], + "choices": ["merged", "replaced", "overridden", "deleted"], "default": "merged", "type": "str" } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/ip_neighbor/ip_neighbor.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/ip_neighbor/ip_neighbor.py new file mode 100644 index 000000000..fef1c67c0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/ip_neighbor/ip_neighbor.py @@ -0,0 +1,56 @@ +# +# -*- 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_ip_neighbor module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Ip_neighborArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_ip_neighbor module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'ipv4_arp_timeout': {'type': 'int'}, + 'ipv4_drop_neighbor_aging_time': {'type': 'int'}, + 'ipv6_drop_neighbor_aging_time': {'type': 'int'}, + 'ipv6_nd_cache_expiry': {'type': 'int'}, + 'num_local_neigh': {'type': 'int'} + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_acls/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_acls/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_acls/l2_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_acls/l2_acls.py new file mode 100644 index 000000000..5b8ba4f87 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l2_acls/l2_acls.py @@ -0,0 +1,129 @@ +# +# -*- 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_l2_acls module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class L2_aclsArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_l2_acls module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'name': {'required': True, 'type': 'str'}, + 'remark': {'type': 'str'}, + 'rules': { + 'elements': 'dict', + 'mutually_exclusive': [['ethertype', 'vlan_tag_format']], + 'options': { + 'action': { + 'choices': ['deny', 'discard', 'do-not-nat', 'permit', 'transit'], + 'type': 'str' + }, + 'dei': { + 'choices': [0, 1], + 'type': 'int' + }, + 'destination': { + 'mutually_exclusive': [['any', 'host', 'address']], + 'options': { + 'address': {'type': 'str'}, + 'address_mask': {'type': 'str'}, + 'any': {'type': 'bool'}, + 'host': {'type': 'str'} + }, + 'required_one_of': [['any', 'host', 'address']], + 'required_together': [['address', 'address_mask']], + 'type': 'dict' + }, + 'ethertype': { + 'mutually_exclusive': [['value', 'arp', 'ipv4', 'ipv6']], + 'options': { + 'arp': {'type': 'bool'}, + 'ipv4': {'type': 'bool'}, + 'ipv6': {'type': 'bool'}, + 'value': {'type': 'str'} + }, + 'type': 'dict' + }, + 'pcp': { + 'mutually_exclusive': [ + ['value', 'traffic_type'], + ['mask', 'traffic_type'] + ], + 'options': { + 'mask': {'type': 'int'}, + 'traffic_type': { + 'choices': ['be', 'bk', 'ee', 'ca', 'vi', 'vo', 'ic', 'nc'], + 'type': 'str' + }, + 'value': {'type': 'int'} + }, + 'required_by': {'mask': ['value']}, + 'type': 'dict' + }, + 'remark': {'type': 'str'}, + 'sequence_num': {'required': True, 'type': 'int'}, + 'source': { + 'mutually_exclusive': [['any', 'host', 'address']], + 'options': { + 'address': {'type': 'str'}, + 'address_mask': {'type': 'str'}, + 'any': {'type': 'bool'}, + 'host': {'type': 'str'} + }, + 'required_one_of': [['any', 'host', 'address']], + 'required_together': [['address', 'address_mask']], + 'type': 'dict' + }, + 'vlan_id': {'type': 'int'}, + 'vlan_tag_format': { + 'options': { + 'multi_tagged': {'type': 'bool'} + }, + 'type': 'dict' + } + }, + 'required_together': [['action', 'source', 'destination']], + 'type': 'list' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 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 index bbebe2d54..b5ce4ad8e 100644 --- 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 @@ -53,7 +53,7 @@ class L2_interfacesArgs(object): # pylint: disable=R0903 'allowed_vlans': { 'elements': 'dict', 'options': { - 'vlan': {'type': 'int'} + 'vlan': {'type': 'str'} }, 'type': 'list' } @@ -64,7 +64,7 @@ class L2_interfacesArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str' } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_acls/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_acls/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_acls/l3_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_acls/l3_acls.py new file mode 100644 index 000000000..7339201ef --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/l3_acls/l3_acls.py @@ -0,0 +1,223 @@ +# +# -*- 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_l3_acls module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class L3_aclsArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_l3_acls module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'acls': { + 'elements': 'dict', + 'options': { + 'name': {'required': True, 'type': 'str'}, + 'remark': {'type': 'str'}, + 'rules': { + 'elements': 'dict', + 'options': { + 'action': { + 'choices': ['deny', 'discard', 'do-not-nat', 'permit', 'transit'], + 'type': 'str' + }, + 'destination': { + 'mutually_exclusive': [['any', 'host', 'prefix']], + 'options': { + 'any': {'type': 'bool'}, + 'host': {'type': 'str'}, + 'port_number': { + 'mutually_exclusive': [['eq', 'gt', 'lt', 'range']], + 'options': { + 'eq': {'type': 'int'}, + 'gt': {'type': 'int'}, + 'lt': {'type': 'int'}, + 'range': { + 'options': { + 'begin': {'type': 'int'}, + 'end': {'type': 'int'} + }, + 'required_together': [['begin', 'end']], + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'prefix': {'type': 'str'} + }, + 'required_one_of': [['any', 'host', 'prefix']], + 'type': 'dict' + }, + 'dscp': { + 'mutually_exclusive': [[ + 'value', 'af11', 'af12', 'af13', 'af21', 'af22', 'af23', 'af31', 'af32', 'af33', + 'cs1', 'cs2', 'cs3', 'cs4', 'cs5', 'cs6', 'cs7', 'default', 'ef', 'voice_admit' + ]], + 'options': { + 'af11': {'type': 'bool'}, + 'af12': {'type': 'bool'}, + 'af13': {'type': 'bool'}, + 'af21': {'type': 'bool'}, + 'af22': {'type': 'bool'}, + 'af23': {'type': 'bool'}, + 'af31': {'type': 'bool'}, + 'af32': {'type': 'bool'}, + 'af33': {'type': 'bool'}, + 'af41': {'type': 'bool'}, + 'af42': {'type': 'bool'}, + 'af43': {'type': 'bool'}, + 'cs1': {'type': 'bool'}, + 'cs2': {'type': 'bool'}, + 'cs3': {'type': 'bool'}, + 'cs4': {'type': 'bool'}, + 'cs5': {'type': 'bool'}, + 'cs6': {'type': 'bool'}, + 'cs7': {'type': 'bool'}, + 'default': {'type': 'bool'}, + 'ef': {'type': 'bool'}, + 'value': {'type': 'int'}, + 'voice_admit': {'type': 'bool'} + }, + 'type': 'dict' + }, + 'protocol': { + 'mutually_exclusive': [['name', 'number']], + 'options': { + 'name': { + 'choices': ['ip', 'ipv6', 'icmp', 'icmpv6', 'tcp', 'udp'], + 'type': 'str' + }, + 'number': {'type': 'int'} + }, + 'required_one_of': [['name', 'number']], + 'type': 'dict' + }, + 'protocol_options': { + 'mutually_exclusive': [['icmp', 'icmpv6', 'tcp']], + 'options': { + 'icmp': { + 'options': { + 'code': {'type': 'int'}, + 'type': {'type': 'int'} + }, + 'type': 'dict' + }, + 'icmpv6': { + 'options': { + 'code': {'type': 'int'}, + 'type': {'type': 'int'} + }, + 'type': 'dict' + }, + 'tcp': { + 'mutually_exclusive': [ + ['established', 'ack', 'not_ack'], + ['established', 'fin', 'not_fin'], + ['established', 'psh', 'not_psh'], + ['established', 'rst', 'not_rst'], + ['established', 'syn', 'not_syn'], + ['established', 'urg', 'not_urg'] + ], + 'options': { + 'ack': {'type': 'bool'}, + 'established': {'type': 'bool'}, + 'fin': {'type': 'bool'}, + 'not_ack': {'type': 'bool'}, + 'not_fin': {'type': 'bool'}, + 'not_psh': {'type': 'bool'}, + 'not_rst': {'type': 'bool'}, + 'not_syn': {'type': 'bool'}, + 'not_urg': {'type': 'bool'}, + 'psh': {'type': 'bool'}, + 'rst': {'type': 'bool'}, + 'syn': {'type': 'bool'}, + 'urg': {'type': 'bool'} + }, + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'remark': {'type': 'str'}, + 'sequence_num': {'required': True, 'type': 'int'}, + 'source': { + 'mutually_exclusive': [['any', 'host', 'prefix']], + 'options': { + 'any': {'type': 'bool'}, + 'host': {'type': 'str'}, + 'port_number': { + 'mutually_exclusive': [['eq', 'gt', 'lt', 'range']], + 'options': { + 'eq': {'type': 'int'}, + 'gt': {'type': 'int'}, + 'lt': {'type': 'int'}, + 'range': { + 'options': { + 'begin': {'type': 'int'}, + 'end': {'type': 'int'} + }, + 'required_together': [['begin', 'end']], + 'type': 'dict' + } + }, + 'type': 'dict' + }, + 'prefix': {'type': 'str'} + }, + 'required_one_of': [['any', 'host', 'prefix']], + 'type': 'dict' + }, + 'vlan_id': {'type': 'int'} + }, + 'required_together': [['action', 'protocol', 'source', 'destination']], + 'type': 'list' + } + }, + 'type': 'list' + }, + 'address_family': { + 'choices': ['ipv4', 'ipv6'], + 'required': True, + 'type': 'str' + } + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 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 index 6e83289cc..b32d7f92e 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -74,7 +74,7 @@ class L3_interfacesArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str' } 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 index 867d61a27..fc349232f 100644 --- 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 @@ -60,7 +60,7 @@ class Lag_interfacesArgs(object): # pylint: disable=R0903 "type": "list" }, "state": { - "choices": ["merged", "deleted"], + "choices": ["merged", "replaced", "overridden", "deleted"], "default": "merged", "type": "str" } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lldp_global/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lldp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lldp_global/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lldp_global/lldp_global.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lldp_global/lldp_global.py new file mode 100644 index 000000000..8f9cd9af0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/lldp_global/lldp_global.py @@ -0,0 +1,81 @@ +# +# -*- 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_lldp_global module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Lldp_globalArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_lldp_global module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'enable': { + 'type': 'bool' + }, + 'hello_time': { + 'type': 'int' + }, + 'mode': { + 'choices': ['receive', 'transmit'], + 'type': 'str' + }, + 'multiplier': { + 'type': 'int' + }, + 'system_description': { + 'type': 'str' + }, + 'system_name': { + 'type': 'str' + }, + 'tlv_select': { + 'options': { + 'management_address': { + 'type': 'bool' + }, + 'system_capabilities': { + 'type': 'bool' + } + }, + '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/logging/logging.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/logging/logging.py new file mode 100644 index 000000000..a83d9eef4 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/logging/logging.py @@ -0,0 +1,64 @@ +# +# -*- 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_logging module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class LoggingArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_logging module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'remote_servers': { + 'elements': 'dict', + 'options': { + 'host': {'required': True, + 'type': 'str'}, + 'message_type': {'choices': ['log', 'event'], + 'type': 'str'}, + 'remote_port': {'type': 'int'}, + 'source_interface': {'type': 'str'}, + 'vrf': {'type': 'str'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', "replaced", "overridden", 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mac/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mac/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mac/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mac/mac.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mac/mac.py new file mode 100644 index 000000000..d46155377 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/mac/mac.py @@ -0,0 +1,66 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_mac module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class MacArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_mac module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'mac': { + 'options': { + 'aging_time': {'default': '600', 'type': 'int'}, + 'dampening_interval': {'default': '5', 'type': 'int'}, + 'dampening_threshold': {'default': '5', 'type': 'int'}, + 'mac_table_entries': { + 'elements': 'dict', + 'options': { + 'interface': {'type': 'str'}, + 'mac_address': {'required': True, 'type': 'str'}, + 'vlan_id': {'required': True, 'type': 'int'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'vrf_name': {'default': 'default', 'type': 'str'} + }, + 'type': 'list' + }, + 'state': {'choices': ['merged', 'deleted', 'replaced', 'overridden'], '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 index be3c38ca2..0d9b45eb0 100644 --- 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 @@ -41,6 +41,8 @@ class MclagArgs(object): # pylint: disable=R0903 'config': { 'options': { 'domain_id': {'required': True, 'type': 'int'}, + 'gateway_mac': {'type': 'str'}, + 'delay_restore': {'type': 'int'}, 'keepalive': {'type': 'int'}, 'peer_address': {'type': 'str'}, 'peer_link': {'type': 'str'}, @@ -56,6 +58,18 @@ class MclagArgs(object): # pylint: disable=R0903 }, 'type': 'dict' }, + 'peer_gateway': { + 'options': { + 'vlans': { + 'elements': 'dict', + 'options': { + 'vlan': {'type': 'str'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, 'session_timeout': {'type': 'int'}, 'source_address': {'type': 'str'}, 'system_mac': {'type': 'str'}, @@ -75,7 +89,7 @@ class MclagArgs(object): # pylint: disable=R0903 'type': 'dict' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str' } 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 index 062520af9..0d357cc2e 100644 --- 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 @@ -64,8 +64,10 @@ class NtpArgs(object): # pylint: disable=R0903 'type': 'str'}, 'key_id': {'type': 'int', 'no_log': True}, 'maxpoll': {'type': 'int'}, - 'minpoll': {'type': 'int'} + 'minpoll': {'type': 'int'}, + 'prefer': {'type': 'bool'} }, + 'required_together': [['minpoll', 'maxpoll']], 'type': 'list' }, 'source_interfaces': { @@ -82,7 +84,7 @@ class NtpArgs(object): # pylint: disable=R0903 'type': 'dict' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', "replaced", "overridden", 'deleted'], 'default': 'merged', 'type': 'str' } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/pki/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/pki/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/pki/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/pki/pki.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/pki/pki.py new file mode 100644 index 000000000..5f4aa32af --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/pki/pki.py @@ -0,0 +1,78 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 Dell EMC +# 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_pki module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class PkiArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_pki module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'security_profiles': { + 'elements': 'dict', + 'options': + { + 'cdp_list': {'elements': 'str', 'type': 'list'}, + 'certificate_name': {'type': 'str'}, + 'key_usage_check': {'type': 'bool'}, + 'ocsp_responder_list': + { + 'elements': 'str', + 'type': 'list' + }, + 'peer_name_check': {'type': 'bool'}, + 'profile_name': {'required': True, 'type': 'str'}, + 'revocation_check': {'type': 'bool'}, + 'trust_store': {'type': 'str'} + }, + 'type': 'list' + }, + 'trust_stores': { + 'elements': 'dict', + 'options': { + 'ca_name': {'elements': 'str', 'type': 'list'}, + 'name': {'required': True, 'type': 'str'} + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], + '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 index 3b8f4a5a3..90d736ff3 100644 --- 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 @@ -42,8 +42,10 @@ class Port_breakoutArgs(object): # pylint: disable=R0903 'elements': 'dict', 'options': { 'mode': { - 'choices': ['1x100G', '1x400G', '1x40G', '2x100G', '2x200G', - '2x50G', '4x100G', '4x10G', '4x25G', '4x50G'], + 'choices': ['1x10G', '1x25G', '1x40G', '1x50G', '1x100G', + '1x200G', '1x400G', '2x10G', '2x25G', '2x40G', + '2x50G', '2x100G', '2x200G', '4x10G', '4x25G', + '4x50G', '4x100G', '8x10G', '8x25G', '8x50G'], 'type': 'str' }, 'name': {'required': True, 'type': 'str'} @@ -51,7 +53,7 @@ class Port_breakoutArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged' } } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_group/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_group/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_group/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_group/port_group.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_group/port_group.py new file mode 100644 index 000000000..9db29de2e --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/port_group/port_group.py @@ -0,0 +1,66 @@ +# +# -*- 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_port_group module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Port_groupArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_port_group module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'id': {'required': True, 'type': 'str'}, + 'speed': {'choices': ['SPEED_10MB', + 'SPEED_100MB', + 'SPEED_1GB', + 'SPEED_2500MB', + 'SPEED_5GB', + 'SPEED_10GB', + 'SPEED_20GB', + 'SPEED_25GB', + 'SPEED_40GB', + 'SPEED_50GB', + 'SPEED_100GB', + 'SPEED_400GB'], + 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 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 index d043ae6f8..17de2eeae 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2019 Red Hat +# Copyright 2023 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) @@ -64,7 +64,7 @@ class Prefix_listsArgs: # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str' } 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 index a56147a5b..0ef029d7b 100644 --- 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 @@ -59,7 +59,7 @@ class Radius_serverArgs(object): # pylint: disable=R0903 }, 'key': {'type': 'str', 'no_log': True}, 'name': {'type': 'str'}, - 'port': {'type': 'int'}, + 'port': {'type': 'int', 'default': 1812}, 'priority': {'type': 'int'}, 'retransmit': {'type': 'int'}, 'source_interface': {'type': 'str'}, @@ -72,12 +72,12 @@ class Radius_serverArgs(object): # pylint: disable=R0903 'type': 'dict' }, 'statistics': {'type': 'bool'}, - 'timeout': {'type': 'int'} + 'timeout': {'type': 'int', 'default': 5} }, 'type': 'dict' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], 'default': 'merged' } } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/route_maps/route_maps.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/route_maps/route_maps.py new file mode 100644 index 000000000..f36fbbcb0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/route_maps/route_maps.py @@ -0,0 +1,196 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_route_maps module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Route_mapsArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_route_maps module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'map_name': {'required': True, 'type': 'str'}, + 'action': { + 'choices': ['permit', 'deny'], + 'type': 'str' + }, + 'sequence_num': { + 'type': 'int' + }, + 'match': { + 'options': { + 'as_path': {'type': 'str'}, + 'community': {'type': 'str'}, + 'evpn': { + 'options': { + 'default_route': {'type': 'bool'}, + 'route_type': { + 'choices': ['macip', 'multicast', 'prefix'], + 'type': 'str' + }, + 'vni': {'type': 'int'} + }, + 'required_one_of': [['default_route', 'route_type', 'vni']], + 'type': 'dict' + }, + 'ext_comm': {'type': 'str'}, + 'interface': {'type': 'str'}, + 'ip': { + 'options': { + 'address': {'type': 'str'}, + 'next_hop': {'type': 'str'} + }, + 'required_one_of': [['address', 'next_hop']], + 'type': 'dict' + }, + 'ipv6': { + 'options': { + 'address': { + 'required': True, + 'type': 'str' + } + }, + 'type': 'dict' + }, + 'local_preference': {'type': 'int'}, + 'metric': {'type': 'int'}, + 'origin': { + 'choices': ['egp', 'igp', 'incomplete'], + 'type': 'str' + }, + 'peer': { + 'mutually_exclusive': [['ip', 'ipv6', 'interface']], + 'options': { + 'interface': {'type': 'str'}, + 'ip': {'type': 'str'}, + 'ipv6': {'type': 'str'} + }, + 'required_one_of': [['ip', 'ipv6', 'interface']], + 'type': 'dict' + }, + 'source_protocol': { + 'choices': ['bgp', 'connected', 'ospf', 'static'], + 'type': 'str' + }, + 'source_vrf': {'type': 'str'}, + 'tag': {'type': 'int'} + }, + 'type': 'dict' + }, + 'set': { + 'options': { + 'as_path_prepend': {'type': 'str'}, + 'comm_list_delete': {'type': 'str'}, + 'community': { + 'options': { + 'community_number': { + 'elements': 'str', + 'type': 'list' + }, + 'community_attributes': { + 'elements': 'str', + 'type': 'list', + 'mutually_exclusive': [ + ['none', 'local_as'], + ['none', 'no_advertise'], + ['none', 'no_export'], + ['none', 'no_peer'], + ['none', 'additive'] + ], + 'choices': [ + 'local_as', + 'no_advertise', + 'no_export', + 'no_peer', + 'additive', + 'none' + ] + }, + }, + 'type': 'dict' + }, + 'extcommunity': { + 'options': { + 'rt': { + 'elements': 'str', + 'type': 'list' + }, + 'soo': { + 'elements': 'str', + 'type': 'list' + } + }, + 'required_one_of': [['rt', 'soo']], + 'type': 'dict' + }, + 'ip_next_hop': {'type': 'str'}, + 'ipv6_next_hop': { + 'options': { + 'global_addr': {'type': 'str'}, + 'prefer_global': {'type': 'bool'} + }, + 'required_one_of': [['global_addr', 'prefer_global']], + 'type': 'dict'}, + 'local_preference': {'type': 'int'}, + 'metric': { + 'mutually_exclusive': [['value', 'rtt_action']], + 'required_one_of': [['value', 'rtt_action']], + 'options': { + 'rtt_action': { + 'choices': ['set', 'add', 'subtract'], + 'type': 'str' + }, + 'value': {'type': 'int'} + }, + 'type': 'dict' + }, + 'origin': { + 'choices': ['egp', 'igp', 'incomplete'], + 'type': 'str' + }, + 'weight': {'type': 'int'} + }, + 'type': 'dict' + }, + 'call': {'type': 'str'}, + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 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 index a146f1ecd..3dbaf3045 100644 --- 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 @@ -72,7 +72,7 @@ class Static_routesArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'overridden', 'replaced'], 'default': 'merged', 'type': 'str' } diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/stp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/stp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/stp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/stp/stp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/stp/stp.py new file mode 100644 index 000000000..145632051 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/stp/stp.py @@ -0,0 +1,152 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_stp module +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class StpArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_stp module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'mutually_exclusive': [['mstp', 'pvst', 'rapid_pvst']], + 'options': { + 'global': { + 'options': { + 'bpdu_filter': {'default': False, 'type': 'bool'}, + 'bridge_priority': {'default': 32768, 'type': 'int'}, + 'disabled_vlans': {'elements': 'str', 'type': 'list'}, + 'enabled_protocol': {'choices': ['mst', 'pvst', 'rapid_pvst'], 'type': 'str'}, + 'fwd_delay': {'default': 15, 'type': 'int'}, + 'hello_time': {'default': 2, 'type': 'int'}, + 'loop_guard': {'default': False, 'type': 'bool'}, + 'max_age': {'default': 20, 'type': 'int'}, + 'portfast': {'default': False, 'type': 'bool'}, + 'root_guard_timeout': {'type': 'int'} + }, + 'type': 'dict' + }, + 'interfaces': { + 'elements': 'dict', + 'options': { + 'bpdu_filter': {'default': False, 'type': 'bool'}, + 'bpdu_guard': {'default': False, 'type': 'bool'}, + 'cost': {'type': 'int'}, + 'edge_port': {'default': False, 'type': 'bool'}, + 'guard': {'choices': ['loop', 'root', 'none'], 'type': 'str'}, + 'intf_name': {'required': True, 'type': 'str'}, + 'link_type': {'choices': ['point-to-point', 'shared'], 'type': 'str'}, + 'port_priority': {'type': 'int'}, + 'portfast': {'default': False, 'type': 'bool'}, + 'shutdown': {'default': False, 'type': 'bool'}, + 'stp_enable': {'default': True, 'type': 'bool'}, + 'uplink_fast': {'default': False, 'type': 'bool'} + }, + 'type': 'list' + }, + 'mstp': { + 'options': { + 'fwd_delay': {'type': 'int'}, + 'hello_time': {'type': 'int'}, + 'max_age': {'type': 'int'}, + 'max_hop': {'type': 'int'}, + 'mst_instances': { + 'elements': 'dict', + 'options': { + 'bridge_priority': {'type': 'int'}, + 'mst_id': {'required': True, 'type': 'int'}, + 'vlans': {'elements': 'str', 'type': 'list'}, + 'interfaces': { + 'elements': 'dict', + 'options': { + 'cost': {'type': 'int'}, + 'intf_name': {'required': True, 'type': 'str'}, + 'port_priority': {'type': 'int'} + }, + 'type': 'list' + } + }, + 'type': 'list' + }, + 'mst_name': {'type': 'str'}, + 'revision': {'type': 'int'} + }, + 'type': 'dict' + }, + 'pvst': { + 'elements': 'dict', + 'options': { + 'bridge_priority': {'type': 'int'}, + 'fwd_delay': {'type': 'int'}, + 'hello_time': {'type': 'int'}, + 'vlan_id': {'required': True, 'type': 'int'}, + 'max_age': {'type': 'int'}, + 'interfaces': { + 'elements': 'dict', + 'options': { + 'cost': {'type': 'int'}, + 'intf_name': {'required': True, 'type': 'str'}, + 'port_priority': {'type': 'int'} + }, + 'type': 'list' + } + }, + 'type': 'list' + }, + 'rapid_pvst': { + 'elements': 'dict', + 'options': { + 'bridge_priority': {'type': 'int'}, + 'fwd_delay': {'type': 'int'}, + 'hello_time': {'type': 'int'}, + 'vlan_id': {'required': True, 'type': 'int'}, + 'max_age': {'type': 'int'}, + 'interfaces': { + 'elements': 'dict', + 'options': { + 'cost': {'type': 'int'}, + 'intf_name': {'required': True, 'type': 'str'}, + 'port_priority': {'type': 'int'} + }, + 'type': 'list' + } + }, + 'type': 'list' + } + }, + 'type': 'dict' + }, + 'state': { + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], + 'default': 'merged', 'type': 'str' + } + } # pylint: disable=C0301 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 index b08c5f4bc..df835c156 100644 --- 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 @@ -57,7 +57,7 @@ class SystemArgs(object): # pylint: disable=R0903 'type': 'dict' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], 'default': 'merged', 'type': 'str' } 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 index aad1746d4..98df26913 100644 --- 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 @@ -69,12 +69,12 @@ class Tacacs_serverArgs(object): # pylint: disable=R0903 'type': 'dict' }, 'source_interface': {'type': 'str'}, - 'timeout': {'type': 'int'} + 'timeout': {'type': 'int', 'default': 5} }, 'type': 'dict' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'replaced', 'overridden', '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 index db23d78e0..7adca72a1 100644 --- 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 @@ -44,7 +44,7 @@ class UsersArgs(object): # pylint: disable=R0903 'name': {'required': True, 'type': 'str'}, 'password': {'type': 'str', 'no_log': True}, 'role': { - 'choices': ['admin', 'operator'], + 'choices': ['admin', 'operator', 'netadmin', 'secadmin'], 'type': 'str' }, 'update_password': { @@ -56,7 +56,7 @@ class UsersArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'overridden', 'replaced'], 'default': 'merged' } } # pylint: disable=C0301 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlan_mapping/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlan_mapping/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlan_mapping/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlan_mapping/vlan_mapping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlan_mapping/vlan_mapping.py new file mode 100644 index 000000000..ced5833fa --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/argspec/vlan_mapping/vlan_mapping.py @@ -0,0 +1,64 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_vlan_mapping module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Vlan_mappingArgs(object): # pylint: disable=R0903 + """The arg spec for the sonic_vlan_mapping module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'mapping': { + 'elements': 'dict', + 'options': { + 'dot1q_tunnel': {'type': 'bool', 'default': False}, + 'inner_vlan': {'type': 'int'}, + 'priority': {'type': 'int'}, + 'service_vlan': {'required': True, 'type': 'int'}, + 'vlan_ids': {'elements': 'str', 'type': 'list'} + }, + 'type': 'list' + }, + 'name': {'required': True, 'type': 'str'} + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 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 index 971fc8571..7ae8b5a47 100644 --- 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 @@ -47,7 +47,7 @@ class VlansArgs(object): # pylint: disable=R0903 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'replaced', 'overridden', '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 index e074936a7..992906044 100644 --- 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 @@ -59,7 +59,7 @@ class VrfsArgs(object): # pylint: disable=R0903 "type": "list" }, "state": { - "choices": ["merged", "deleted"], + "choices": ["merged", "replaced", "overridden", "deleted"], "default": "merged", "type": "str" } 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 index dd475b78a..e610eaca8 100644 --- 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 @@ -62,11 +62,10 @@ class VxlansArgs(object): # pylint: disable=R0903 'type': 'list' } }, - 'required_together': [['source_ip', 'evpn_nvo']], 'type': 'list' }, 'state': { - 'choices': ['merged', 'deleted'], + 'choices': ['merged', 'deleted', 'replaced', 'overridden'], 'default': 'merged', 'type': 'str' } 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 index 85f93bc73..036567f0a 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2021 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -31,6 +31,10 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s to_request, edit_config ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + get_new_config, + get_formatted_config_diff +) PATCH = 'patch' DELETE = 'delete' @@ -89,6 +93,16 @@ class Aaa(ConfigBase): if result['changed']: result['after'] = changed_aaa_facts + new_config = changed_aaa_facts + old_config = existing_aaa_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_aaa_facts) + result['after(generated)'] = new_config + if self._module._diff: + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -123,15 +137,22 @@ class Aaa(ConfigBase): state = self._module.params['state'] if not want: want = {} + if not have: + have = {} + + diff = self.get_diff_aaa(want, have) if state == 'deleted': commands = self._state_deleted(want, have) elif state == 'merged': - diff = get_diff(want, have) - commands = self._state_merged(want, have, diff) + commands = self._state_merged(diff) + elif state == 'replaced': + commands = self._state_replaced(diff) + elif state == 'overridden': + commands = self._state_overridden(want, have) return commands - def _state_merged(self, want, have, diff): + def _state_merged(self, diff): """ The command generator when state is merged :rtype: A list @@ -171,6 +192,49 @@ class Aaa(ConfigBase): commands = update_states(diff_want, "deleted") return commands, requests + def _state_replaced(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 = [] + requests = [] + if diff: + requests = self.get_create_aaa_request(diff) + if len(requests) > 0: + commands = update_states(diff, "replaced") + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + if have and have != want: + del_requests = self.get_delete_all_aaa_request(have) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + mod_commands = want + mod_requests = self.get_create_aaa_request(mod_commands) + + if len(mod_requests) > 0: + requests.extend(mod_requests) + commands.extend(update_states(mod_commands, "overridden")) + + return commands, requests + def get_create_aaa_request(self, commands): requests = [] aaa_path = 'data/openconfig-system:system/aaa' @@ -183,13 +247,17 @@ class Aaa(ConfigBase): def build_create_aaa_payload(self, commands): payload = {} + auth_method_list = [] if "authentication" in commands and commands["authentication"]: - payload = {"openconfig-system:aaa": {"authentication": {"config": {"authentication-method": []}}}} + payload = {"openconfig-system:aaa": {"authentication": {"config": {}}}} if "local" in commands["authentication"]["data"] and commands["authentication"]["data"]["local"]: - payload['openconfig-system:aaa']['authentication']['config']['authentication-method'].append("local") + auth_method_list.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) + auth_method_list.append(auth_method) + if auth_method_list: + cfg = {'authentication-method': auth_method_list} + payload['openconfig-system:aaa']['authentication']['config'].update(cfg) if "fail_through" in commands["authentication"]["data"]: cfg = {'failthrough': str(commands["authentication"]["data"]["fail_through"])} payload['openconfig-system:aaa']['authentication']['config'].update(cfg) @@ -234,3 +302,53 @@ class Aaa(ConfigBase): method = DELETE request = {'path': path, 'method': method} return request + + # Current SONiC code behavior for patch overwrites the OC authentication-method leaf-list + # This function serves as a workaround for the issue, allowing the user to append to the + # OC authentication-method leaf-list. + def get_diff_aaa(self, want, have): + diff_cfg = {} + diff_authentication = {} + diff_data = {} + + authentication = want.get('authentication', None) + if authentication: + data = authentication.get('data', None) + if data: + fail_through = data.get('fail_through', None) + local = data.get('local', None) + group = data.get('group', None) + + cfg_authentication = have.get('authentication', None) + if cfg_authentication: + cfg_data = cfg_authentication.get('data', None) + if cfg_data: + cfg_fail_through = cfg_data.get('fail_through', None) + cfg_local = cfg_data.get('local', None) + cfg_group = cfg_data.get('group', None) + + if fail_through is not None and fail_through != cfg_fail_through: + diff_data['fail_through'] = fail_through + if local and local != cfg_local: + diff_data['local'] = local + if group and group != cfg_group: + diff_data['group'] = group + + diff_local = diff_data.get('local', None) + diff_group = diff_data.get('group', None) + if diff_local and not diff_group and cfg_group: + diff_data['group'] = cfg_group + if diff_group and not diff_local and cfg_local: + diff_data['local'] = cfg_local + else: + if fail_through is not None: + diff_data['fail_through'] = fail_through + if local: + diff_data['local'] = local + if group: + diff_data['group'] = group + if diff_data: + diff_authentication['data'] = diff_data + diff_cfg['authentication'] = diff_authentication + + return diff_cfg diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/acl_interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/acl_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/acl_interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/acl_interfaces/acl_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/acl_interfaces/acl_interfaces.py new file mode 100644 index 000000000..0413a585d --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/acl_interfaces/acl_interfaces.py @@ -0,0 +1,499 @@ +# +# -*- 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_acl_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, + remove_empties, + validate_config +) +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_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) +from ansible.module_utils.connection import ConnectionError + +DELETE = 'delete' +POST = 'post' + +TEST_KEYS = [ + {'config': {'name': ''}}, + {'access_groups': {'type': ''}}, + {'acls': {'name': ''}} +] + +TEST_KEYS_formatted_diff = [ + {'config': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'access_groups': {'type': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'acls': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] + +acl_type_to_payload_map = { + 'mac': 'ACL_L2', + 'ipv4': 'ACL_IPV4', + 'ipv6': 'ACL_IPV6' +} + + +class Acl_interfaces(ConfigBase): + """ + The sonic_acl_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'acl_interfaces', + ] + + acl_interfaces_path = 'data/openconfig-acl:acl/interfaces/interface={intf_name}' + ingress_acl_set_path = acl_interfaces_path + '/ingress-acl-sets/ingress-acl-set={acl_name},{acl_type}' + egress_acl_set_path = acl_interfaces_path + '/egress-acl-sets/egress-acl-set={acl_name},{acl_type}' + + def __init__(self, module): + super(Acl_interfaces, self).__init__(module) + + def get_acl_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) + acl_interfaces_facts = facts['ansible_network_resources'].get('acl_interfaces') + if not acl_interfaces_facts: + return [] + return acl_interfaces_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + + existing_acl_interfaces_facts = self.get_acl_interfaces_facts() + commands, requests = self.set_config(existing_acl_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 + + changed_acl_interfaces_facts = self.get_acl_interfaces_facts() + + result['before'] = existing_acl_interfaces_facts + if result['changed']: + result['after'] = changed_acl_interfaces_facts + + result['commands'] = commands + + new_config = changed_acl_interfaces_facts + old_config = existing_acl_interfaces_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_acl_interfaces_facts, + TEST_KEYS_formatted_diff) + self.post_process_generated_config(new_config) + result['after(generated)'] = new_config + if self._module._diff: + self.sort_config(new_config) + self.sort_config(old_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) + result['warnings'] = warnings + return result + + def set_config(self, existing_acl_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'] + if want: + want = self.validate_and_normalize_config(want) + else: + want = [] + + have = existing_acl_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'] + if state == 'overridden': + commands, requests = self._state_overridden(want, have) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have) + + return commands, requests + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + del_commands = [] + add_commands = [] + + have_interfaces = self.get_interface_names(have) + want_interfaces = self.get_interface_names(want) + interfaces_to_replace = have_interfaces.intersection(want_interfaces) + + del_diff = get_diff(have, want, TEST_KEYS) + for cmd in del_diff: + if cmd['name'] in interfaces_to_replace: + del_commands.append(cmd) + + if del_commands: + commands = update_states(del_commands, 'deleted') + requests.extend(self.get_interfaces_acl_unbind_requests(del_commands)) + + add_diff = get_diff(want, have, TEST_KEYS) + # Handle scenarios in replaced state, when only the interface + # name is specified for deleting all ACL bindings in it. + for cmd in add_diff: + if cmd.get('access_groups'): + add_commands.append(cmd) + + if add_commands: + commands.extend(update_states(add_commands, 'replaced')) + requests.extend(self.get_interfaces_acl_bind_requests(add_commands)) + + return commands, requests + + def _state_overridden(self, want, have): + """ 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 = [] + del_commands = [] + + have_interfaces = self.get_interface_names(have) + want_interfaces = self.get_interface_names(want) + interfaces_to_delete = have_interfaces.difference(want_interfaces) + interfaces_to_override = have_interfaces.intersection(want_interfaces) + + del_diff = get_diff(have, want, TEST_KEYS) + for cmd in del_diff: + if cmd['name'] in interfaces_to_delete: + del_commands.append({'name': cmd['name']}) + elif cmd['name'] in interfaces_to_override: + del_commands.append(cmd) + + if del_commands: + commands = update_states(del_commands, 'deleted') + requests.extend(self.get_interfaces_acl_unbind_requests(del_commands)) + + diff = get_diff(want, have, TEST_KEYS) + if diff: + commands.extend(update_states(diff, 'overridden')) + requests.extend(self.get_interfaces_acl_bind_requests(diff)) + + return commands, requests + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + requests = [] + + diff = get_diff(want, have, TEST_KEYS) + if diff: + requests = self.get_interfaces_acl_bind_requests(diff) + 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: + # Delete all interface ACL bindings in the chassis + for cfg in have: + commands.append({'name': cfg['name']}) + else: + want_dict = self._convert_config_list_to_dict(want) + have_dict = self._convert_config_list_to_dict(have) + + for intf_name, access_groups in want_dict.items(): + have_obj = have_dict.get(intf_name) + if not have_obj: + continue + + if not access_groups: + commands.append({'name': intf_name}) + else: + access_groups_to_del = [] + for acl_type, acls in access_groups.items(): + acls_to_delete = [] + if not have_obj.get(acl_type): + continue + + # Delete all bindings of ACLs belonging to a type in an + # interface, if only the ACL type is provided + if not acls: + for acl_name, direction in have_obj[acl_type].items(): + acls_to_delete.append({'name': acl_name, 'direction': direction}) + else: + for acl_name, direction in acls.items(): + if have_obj[acl_type].get(acl_name) and direction == have_obj[acl_type][acl_name]: + acls_to_delete.append({'name': acl_name, 'direction': direction}) + + if acls_to_delete: + access_groups_to_del.append({'type': acl_type, 'acls': acls_to_delete}) + + if access_groups_to_del: + commands.append({'name': intf_name, 'access_groups': access_groups_to_del}) + + if commands: + requests = self.get_interfaces_acl_unbind_requests(commands) + commands = update_states(commands, 'deleted') + + return commands, requests + + def get_interfaces_acl_bind_requests(self, commands): + """Get requests to bind specified ACLs for all interfaces + specified the commands + """ + requests = [] + + for command in commands: + intf_name = command['name'] + url = self.acl_interfaces_path.format(intf_name=intf_name) + for access_group in command['access_groups']: + for acl in access_group['acls']: + if acl['direction'] == 'in': + payload = { + 'openconfig-acl:config': { + 'id': intf_name + }, + 'openconfig-acl:interface-ref': { + 'config': { + 'interface': intf_name.split('.')[0] + } + }, + 'openconfig-acl:ingress-acl-sets': { + 'ingress-acl-set': [ + { + 'set-name': acl['name'], + 'type': acl_type_to_payload_map[access_group['type']], + 'config': { + 'set-name': acl['name'], + 'type': acl_type_to_payload_map[access_group['type']] + } + } + ] + } + } + else: + payload = { + 'openconfig-acl:config': { + 'id': intf_name + }, + 'openconfig-acl:interface-ref': { + 'config': { + 'interface': intf_name.split('.')[0] + } + }, + 'openconfig-acl:egress-acl-sets': { + 'egress-acl-set': [ + { + 'set-name': acl['name'], + 'type': acl_type_to_payload_map[access_group['type']], + 'config': { + 'set-name': acl['name'], + 'type': acl_type_to_payload_map[access_group['type']] + } + } + ] + } + } + + # Update the payload for subinterfaces + if '.' in intf_name: + payload['openconfig-acl:interface-ref']['config']['subinterface'] = int(intf_name.split('.')[1]) + + requests.append({'path': url, 'method': POST, 'data': payload}) + + return requests + + def get_interfaces_acl_unbind_requests(self, commands): + """Get requests to unbind specified ACLs for all interfaces + specified in the commands + """ + requests = [] + + for command in commands: + intf_name = command['name'] + # Delete all acl bindings in an interface, if only the + # interface name is provided + if not command.get('access_groups'): + url = self.acl_interfaces_path.format(intf_name=intf_name) + requests.append({'path': url, 'method': DELETE}) + else: + for access_group in command['access_groups']: + for acl in access_group['acls']: + if acl['direction'] == 'in': + url = self.ingress_acl_set_path.format(intf_name=intf_name, acl_name=acl['name'], + acl_type=acl_type_to_payload_map[access_group['type']]) + requests.append({'path': url, 'method': DELETE}) + else: + url = self.egress_acl_set_path.format(intf_name=intf_name, acl_name=acl['name'], + acl_type=acl_type_to_payload_map[access_group['type']]) + requests.append({'path': url, 'method': DELETE}) + + return requests + + def validate_and_normalize_config(self, config_list): + """Validate and normalize the given config""" + # Remove empties and validate the config with argument spec + config_list = [remove_empties(config) for config in config_list] + validate_config(self._module.argument_spec, {'config': config_list}) + normalize_interface_name(config_list, self._module) + + state = self._module.params['state'] + # When state is deleted, empty access_groups and acls are + # supported and therefore no futher changes are required. + if state == 'deleted': + return config_list + + updated_config_list = [] + for config in config_list: + if not config.get('access_groups'): + # When state is replaced, if only the interface name is + # specified for deleting all ACL bindings in it do not + # remove that config. + if state == 'replaced': + updated_config_list.append(config) + else: + access_group_list = [] + for access_group in config['access_groups']: + if access_group.get('acls'): + access_group_list.append(access_group) + + if access_group_list: + updated_config_list.append({'name': config['name'], 'access_groups': access_group_list}) + + return updated_config_list + + @staticmethod + def get_interface_names(config_list): + """Get a set of interface names available in the given + config_list dict + """ + interface_names = set() + for config in config_list: + interface_names.add(config['name']) + + return interface_names + + @staticmethod + def _convert_config_list_to_dict(config_list): + config_dict = {} + + for config in config_list: + config_dict[config['name']] = {} + if config.get('access_groups'): + for access_group in config['access_groups']: + config_dict[config['name']][access_group['type']] = {} + if access_group.get('acls'): + for acl in access_group['acls']: + config_dict[config['name']][access_group['type']][acl['name']] = acl['direction'] + + return config_dict + + def sort_config(self, configs): + # natsort provides better result. + # The use of natsort causes sanity error due to it is not available in + # python version currently used. + # new_config = natsorted(new_config, key=lambda x: x['name']) + # For time-being, use simple "sort" + configs.sort(key=lambda x: x['name']) + + for conf in configs: + ags = conf.get('access_groups', []) + if ags: + ags.sort(key=lambda x: x['type']) + for ag in ags: + if ag.get('acls', []): + ag['acls'].sort(key=lambda x: x['name']) + + def post_process_generated_config(self, configs): + for conf in configs[:]: + ags = conf.get('access_groups', []) + if ags: + for ag in ags[:]: + if not ag.get('acls', []): + ags.remove(ag) + + if not conf.get('access_groups', []): + configs.remove(conf) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bfd/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bfd/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bfd/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bfd/bfd.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bfd/bfd.py new file mode 100644 index 000000000..484c4203d --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/bfd/bfd.py @@ -0,0 +1,734 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_bfd 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.utils.utils import ( + get_diff, + get_replaced_config, + send_requests, + remove_empties, + update_states +) +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 copy import deepcopy + + +BFD_PATH = '/data/openconfig-bfd:bfd' +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'profiles': {'profile_name': ''}}, + {'single_hops': {'remote_address': '', 'vrf': '', 'interface': '', 'local_address': ''}}, + {'multi_hops': {'remote_address': '', 'vrf': '', 'local_address': ''}} +] + + +class Bfd(ConfigBase): + """ + The sonic_bfd class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'bfd', + ] + + def __init__(self, module): + super(Bfd, self).__init__(module) + + def get_bfd_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) + bfd_facts = facts['ansible_network_resources'].get('bfd') + if not bfd_facts: + return {} + return bfd_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + commands = [] + + existing_bfd_facts = self.get_bfd_facts() + commands, requests = self.set_config(existing_bfd_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_bfd_facts = self.get_bfd_facts() + + result['before'] = existing_bfd_facts + if result['changed']: + result['after'] = changed_bfd_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bfd_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_bfd_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) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(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 + """ + replaced_config = get_replaced_config(want, have, TEST_KEYS) + + if replaced_config: + self.sort_lists_in_config(replaced_config) + self.sort_lists_in_config(have) + is_delete_all = (replaced_config == have) + requests = self.get_delete_bfd_requests(replaced_config, have, is_delete_all) + send_requests(self._module, requests) + + commands = want + else: + commands = diff + + requests = [] + + if commands: + requests = self.get_modify_bfd_request(commands) + + if len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + + if have and have != want: + is_delete_all = True + requests = self.get_delete_bfd_requests(have, None, is_delete_all) + send_requests(self._module, requests) + have = [] + + commands = [] + requests = [] + + if not have and want: + commands = want + requests = self.get_modify_bfd_request(commands) + + if len(requests) > 0: + commands = update_states(commands, "overridden") + else: + commands = [] + + return 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_bfd_request(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 + :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 + want = remove_empties(want) + if not want: + commands = deepcopy(have) + is_delete_all = True + else: + commands = deepcopy(want) + + self.remove_default_entries(commands) + requests = self.get_delete_bfd_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_bfd_request(self, commands): + request = None + + profiles = commands.get('profiles', None) + single_hops = commands.get('single_hops', None) + multi_hops = commands.get('multi_hops', None) + bfd_dict = {} + bfd_profile_dict = {} + bfd_shop_dict = {} + bfd_mhop_dict = {} + + if profiles: + profile_list = [] + for profile in profiles: + profile_dict = {} + config_dict = {} + profile_name = profile.get('profile_name', None) + enabled = profile.get('enabled', None) + transmit_interval = profile.get('transmit_interval', None) + receive_interval = profile.get('receive_interval', None) + detect_multiplier = profile.get('detect_multiplier', None) + passive_mode = profile.get('passive_mode', None) + min_ttl = profile.get('min_ttl', None) + echo_interval = profile.get('echo_interval', None) + echo_mode = profile.get('echo_mode', None) + + if profile_name: + profile_dict['profile-name'] = profile_name + config_dict['profile-name'] = profile_name + if enabled is not None: + config_dict['enabled'] = enabled + if transmit_interval: + config_dict['desired-minimum-tx-interval'] = transmit_interval + if receive_interval: + config_dict['required-minimum-receive'] = receive_interval + if detect_multiplier: + config_dict['detection-multiplier'] = detect_multiplier + if passive_mode is not None: + config_dict['passive-mode'] = passive_mode + if min_ttl: + config_dict['minimum-ttl'] = min_ttl + if echo_interval: + config_dict['desired-minimum-echo-receive'] = echo_interval + if echo_mode is not None: + config_dict['echo-active'] = echo_mode + if config_dict: + profile_dict['config'] = config_dict + profile_list.append(profile_dict) + if profile_list: + bfd_profile_dict['profile'] = profile_list + + if single_hops: + single_hop_list = [] + for hop in single_hops: + hop_dict = {} + config_dict = {} + remote_address = hop.get('remote_address', None) + vrf = hop.get('vrf', None) + interface = hop.get('interface', None) + local_address = hop.get('local_address', None) + enabled = hop.get('enabled', None) + transmit_interval = hop.get('transmit_interval', None) + receive_interval = hop.get('receive_interval', None) + detect_multiplier = hop.get('detect_multiplier', None) + passive_mode = hop.get('passive_mode', None) + echo_interval = hop.get('echo_interval', None) + echo_mode = hop.get('echo_mode', None) + profile_name = hop.get('profile_name', None) + + if remote_address: + hop_dict['remote-address'] = remote_address + config_dict['remote-address'] = remote_address + if vrf: + hop_dict['vrf'] = vrf + config_dict['vrf'] = vrf + if interface: + hop_dict['interface'] = interface + config_dict['interface'] = interface + if local_address: + hop_dict['local-address'] = local_address + config_dict['local-address'] = local_address + if enabled is not None: + config_dict['enabled'] = enabled + if transmit_interval: + config_dict['desired-minimum-tx-interval'] = transmit_interval + if receive_interval: + config_dict['required-minimum-receive'] = receive_interval + if detect_multiplier: + config_dict['detection-multiplier'] = detect_multiplier + if passive_mode is not None: + config_dict['passive-mode'] = passive_mode + if echo_interval: + config_dict['desired-minimum-echo-receive'] = echo_interval + if echo_mode is not None: + config_dict['echo-active'] = echo_mode + if profile_name: + config_dict['profile-name'] = profile_name + if config_dict: + hop_dict['config'] = config_dict + single_hop_list.append(hop_dict) + if single_hop_list: + bfd_shop_dict['single-hop'] = single_hop_list + + if multi_hops: + multi_hop_list = [] + for hop in multi_hops: + hop_dict = {} + config_dict = {} + remote_address = hop.get('remote_address', None) + vrf = hop.get('vrf', None) + local_address = hop.get('local_address', None) + enabled = hop.get('enabled', None) + transmit_interval = hop.get('transmit_interval', None) + receive_interval = hop.get('receive_interval', None) + detect_multiplier = hop.get('detect_multiplier', None) + passive_mode = hop.get('passive_mode', None) + min_ttl = hop.get('min_ttl', None) + profile_name = hop.get('profile_name', None) + + if remote_address: + hop_dict['remote-address'] = remote_address + config_dict['remote-address'] = remote_address + if vrf: + hop_dict['vrf'] = vrf + config_dict['vrf'] = vrf + if local_address: + hop_dict['local-address'] = local_address + config_dict['local-address'] = local_address + if enabled is not None: + config_dict['enabled'] = enabled + if transmit_interval: + config_dict['desired-minimum-tx-interval'] = transmit_interval + if receive_interval: + config_dict['required-minimum-receive'] = receive_interval + if detect_multiplier: + config_dict['detection-multiplier'] = detect_multiplier + if passive_mode is not None: + config_dict['passive-mode'] = passive_mode + if min_ttl: + config_dict['minimum-ttl'] = min_ttl + if profile_name: + config_dict['profile-name'] = profile_name + if config_dict: + config_dict['interface'] = 'null' + hop_dict['interface'] = 'null' + hop_dict['config'] = config_dict + multi_hop_list.append(hop_dict) + if multi_hop_list: + bfd_mhop_dict['multi-hop'] = multi_hop_list + + if bfd_profile_dict: + bfd_dict['openconfig-bfd-ext:bfd-profile'] = bfd_profile_dict + if bfd_shop_dict: + bfd_dict['openconfig-bfd-ext:bfd-shop-sessions'] = bfd_shop_dict + if bfd_mhop_dict: + bfd_dict['openconfig-bfd-ext:bfd-mhop-sessions'] = bfd_mhop_dict + if bfd_dict: + payload = {'openconfig-bfd:bfd': bfd_dict} + request = {'path': BFD_PATH, 'method': PATCH, 'data': payload} + + return request + + def get_delete_bfd_requests(self, commands, have, is_delete_all): + requests = [] + + if not commands: + return requests + + if is_delete_all: + requests.extend(self.get_delete_all_bfd_cfg_requests(commands)) + else: + requests.extend(self.get_delete_bfd_profile_requests(commands, have)) + requests.extend(self.get_delete_bfd_shop_requests(commands, have)) + requests.extend(self.get_delete_bfd_mhop_requests(commands, have)) + + return requests + + def get_delete_bfd_profile_requests(self, commands, have): + requests = [] + + profiles = commands.get('profiles', None) + if profiles: + for profile in profiles: + profile_name = profile.get('profile_name', None) + enabled = profile.get('enabled', None) + transmit_interval = profile.get('transmit_interval', None) + receive_interval = profile.get('receive_interval', None) + detect_multiplier = profile.get('detect_multiplier', None) + passive_mode = profile.get('passive_mode', None) + min_ttl = profile.get('min_ttl', None) + echo_interval = profile.get('echo_interval', None) + echo_mode = profile.get('echo_mode', None) + + cfg_profiles = have.get('profiles', None) + if cfg_profiles: + for cfg_profile in cfg_profiles: + cfg_profile_name = cfg_profile.get('profile_name', None) + cfg_enabled = cfg_profile.get('enabled', None) + cfg_transmit_interval = cfg_profile.get('transmit_interval', None) + cfg_receive_interval = cfg_profile.get('receive_interval', None) + cfg_detect_multiplier = cfg_profile.get('detect_multiplier', None) + cfg_passive_mode = cfg_profile.get('passive_mode', None) + cfg_min_ttl = cfg_profile.get('min_ttl', None) + cfg_echo_interval = cfg_profile.get('echo_interval', None) + cfg_echo_mode = cfg_profile.get('echo_mode', None) + + if profile_name == cfg_profile_name: + if enabled is not None and enabled == cfg_enabled: + requests.append(self.get_delete_profile_attr_request(profile_name, 'enabled')) + if transmit_interval and transmit_interval == cfg_transmit_interval: + requests.append(self.get_delete_profile_attr_request(profile_name, 'desired-minimum-tx-interval')) + if receive_interval and receive_interval == cfg_receive_interval: + requests.append(self.get_delete_profile_attr_request(profile_name, 'required-minimum-receive')) + if detect_multiplier and detect_multiplier == cfg_detect_multiplier: + requests.append(self.get_delete_profile_attr_request(profile_name, 'detection-multiplier')) + if passive_mode is not None and passive_mode == cfg_passive_mode: + requests.append(self.get_delete_profile_attr_request(profile_name, 'passive-mode')) + if min_ttl and min_ttl == cfg_min_ttl: + requests.append(self.get_delete_profile_attr_request(profile_name, 'minimum-ttl')) + if echo_interval and echo_interval == cfg_echo_interval: + requests.append(self.get_delete_profile_attr_request(profile_name, 'desired-minimum-echo-receive')) + if echo_mode is not None and echo_mode == cfg_echo_mode: + requests.append(self.get_delete_profile_attr_request(profile_name, 'echo-active')) + if (enabled is None and not transmit_interval and not receive_interval and not detect_multiplier and passive_mode is None + and not min_ttl and not echo_interval and echo_mode is None): + requests.append(self.get_delete_profile_request(profile_name)) + + return requests + + def get_delete_bfd_shop_requests(self, commands, have): + requests = [] + + single_hops = commands.get('single_hops', None) + if single_hops: + for hop in single_hops: + remote_address = hop.get('remote_address', None) + vrf = hop.get('vrf', None) + interface = hop.get('interface', None) + local_address = hop.get('local_address', None) + enabled = hop.get('enabled', None) + transmit_interval = hop.get('transmit_interval', None) + receive_interval = hop.get('receive_interval', None) + detect_multiplier = hop.get('detect_multiplier', None) + passive_mode = hop.get('passive_mode', None) + echo_interval = hop.get('echo_interval', None) + echo_mode = hop.get('echo_mode', None) + profile_name = hop.get('profile_name', None) + + cfg_single_hops = have.get('single_hops', None) + if cfg_single_hops: + for cfg_hop in cfg_single_hops: + cfg_remote_address = cfg_hop.get('remote_address', None) + cfg_vrf = cfg_hop.get('vrf', None) + cfg_interface = cfg_hop.get('interface', None) + cfg_local_address = cfg_hop.get('local_address', None) + cfg_enabled = cfg_hop.get('enabled', None) + cfg_transmit_interval = cfg_hop.get('transmit_interval', None) + cfg_receive_interval = cfg_hop.get('receive_interval', None) + cfg_detect_multiplier = cfg_hop.get('detect_multiplier', None) + cfg_passive_mode = cfg_hop.get('passive_mode', None) + cfg_echo_interval = cfg_hop.get('echo_interval', None) + cfg_echo_mode = cfg_hop.get('echo_mode', None) + cfg_profile_name = cfg_hop.get('profile_name', None) + + if remote_address == cfg_remote_address and vrf == cfg_vrf and interface == cfg_interface and local_address == cfg_local_address: + if enabled is not None and enabled == cfg_enabled: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, 'enabled')) + if transmit_interval and transmit_interval == cfg_transmit_interval: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, + 'desired-minimum-tx-interval')) + if receive_interval and receive_interval == cfg_receive_interval: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, 'required-minimum-receive')) + if detect_multiplier and detect_multiplier == cfg_detect_multiplier: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, 'detection-multiplier')) + if passive_mode is not None and passive_mode == cfg_passive_mode: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, 'passive-mode')) + if echo_interval and echo_interval == cfg_echo_interval: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, + 'desired-minimum-echo-receive')) + if echo_mode is not None and echo_mode == cfg_echo_mode: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, 'echo-active')) + if profile_name and profile_name == cfg_profile_name: + requests.append(self.get_delete_shop_attr_request(remote_address, interface, vrf, local_address, 'profile-name')) + if (enabled is None and not transmit_interval and not receive_interval and not detect_multiplier and passive_mode is None + and not echo_interval and echo_mode is None and not profile_name): + requests.append(self.get_delete_shop_request(remote_address, interface, vrf, local_address)) + + return requests + + def get_delete_bfd_mhop_requests(self, commands, have): + requests = [] + + multi_hops = commands.get('multi_hops', None) + if multi_hops: + for hop in multi_hops: + remote_address = hop.get('remote_address', None) + vrf = hop.get('vrf', None) + local_address = hop.get('local_address', None) + enabled = hop.get('enabled', None) + transmit_interval = hop.get('transmit_interval', None) + receive_interval = hop.get('receive_interval', None) + detect_multiplier = hop.get('detect_multiplier', None) + passive_mode = hop.get('passive_mode', None) + min_ttl = hop.get('min_ttl', None) + profile_name = hop.get('profile_name', None) + + cfg_multi_hops = have.get('multi_hops', None) + if cfg_multi_hops: + for cfg_hop in cfg_multi_hops: + cfg_remote_address = cfg_hop.get('remote_address', None) + cfg_vrf = cfg_hop.get('vrf', None) + cfg_local_address = cfg_hop.get('local_address', None) + cfg_enabled = cfg_hop.get('enabled', None) + cfg_transmit_interval = cfg_hop.get('transmit_interval', None) + cfg_receive_interval = cfg_hop.get('receive_interval', None) + cfg_detect_multiplier = cfg_hop.get('detect_multiplier', None) + cfg_passive_mode = cfg_hop.get('passive_mode', None) + cfg_min_ttl = cfg_hop.get('min_ttl', None) + cfg_profile_name = cfg_hop.get('profile_name', None) + + if remote_address == cfg_remote_address and vrf == cfg_vrf and local_address == cfg_local_address: + if enabled is not None and enabled == cfg_enabled: + requests.append(self.get_delete_mhop_attr_request(remote_address, vrf, local_address, 'enabled')) + if transmit_interval and transmit_interval == cfg_transmit_interval: + requests.append(self.get_delete_mhop_attr_request(remote_address, vrf, local_address, 'desired-minimum-tx-interval')) + if receive_interval and receive_interval == cfg_receive_interval: + requests.append(self.get_delete_mhop_attr_request(remote_address, vrf, local_address, 'required-minimum-receive')) + if detect_multiplier and detect_multiplier == cfg_detect_multiplier: + requests.append(self.get_delete_mhop_attr_request(remote_address, vrf, local_address, 'detection-multiplier')) + if passive_mode is not None and passive_mode == cfg_passive_mode: + requests.append(self.get_delete_mhop_attr_request(remote_address, vrf, local_address, 'passive-mode')) + if min_ttl and min_ttl == cfg_min_ttl: + requests.append(self.get_delete_mhop_attr_request(remote_address, vrf, local_address, 'minimum-ttl')) + if profile_name and profile_name == cfg_profile_name: + requests.append(self.get_delete_mhop_attr_request(remote_address, vrf, local_address, 'profile-name')) + if (enabled is None and not transmit_interval and not receive_interval and not detect_multiplier and passive_mode is None + and not min_ttl and not profile_name): + requests.append(self.get_delete_mhop_request(remote_address, vrf, local_address)) + + return requests + + def get_delete_all_bfd_cfg_requests(self, commands): + requests = [] + profiles = commands.get('profiles', None) + single_hops = commands.get('single_hops', None) + multi_hops = commands.get('multi_hops', None) + + if profiles: + url = '%s/openconfig-bfd-ext:bfd-profile/profile' % (BFD_PATH) + requests.append({'path': url, 'method': DELETE}) + if single_hops: + url = '%s/openconfig-bfd-ext:bfd-shop-sessions/single-hop' % (BFD_PATH) + requests.append({'path': url, 'method': DELETE}) + if multi_hops: + url = '%s/openconfig-bfd-ext:bfd-mhop-sessions/multi-hop' % (BFD_PATH) + requests.append({'path': url, 'method': DELETE}) + + return requests + + def get_delete_profile_request(self, profile_name): + url = '%s/openconfig-bfd-ext:bfd-profile/profile=%s' % (BFD_PATH, profile_name) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_profile_attr_request(self, profile_name, attr): + url = '%s/openconfig-bfd-ext:bfd-profile/profile=%s/config/%s' % (BFD_PATH, profile_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_shop_request(self, remote_address, interface, vrf, local_address): + url = '%s/openconfig-bfd-ext:bfd-shop-sessions/single-hop=%s,%s,%s,%s' % (BFD_PATH, remote_address, interface, vrf, local_address) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_shop_attr_request(self, remote_address, interface, vrf, local_address, attr): + url = '%s/openconfig-bfd-ext:bfd-shop-sessions/single-hop=%s,%s,%s,%s/config/%s' % (BFD_PATH, remote_address, interface, vrf, local_address, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mhop_request(self, remote_address, vrf, local_address): + url = '%s/openconfig-bfd-ext:bfd-mhop-sessions/multi-hop=%s,null,%s,%s' % (BFD_PATH, remote_address, vrf, local_address) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mhop_attr_request(self, remote_address, vrf, local_address, attr): + url = '%s/openconfig-bfd-ext:bfd-mhop-sessions/multi-hop=%s,null,%s,%s/config/%s' % (BFD_PATH, remote_address, vrf, local_address, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_profile_name(self, profile_name): + return profile_name.get('profile_name') + + def sort_lists_in_config(self, config): + if 'profiles' in config and config['profiles'] is not None: + config['profiles'].sort(key=self.get_profile_name) + if 'single_hops' in config and config['single_hops'] is not None: + config['single_hops'].sort(key=lambda x: (x['remote_address'], x['interface'], x['vrf'], x['local_address'])) + if 'multi_hops' in config and config['multi_hops'] is not None: + config['multi_hops'].sort(key=lambda x: (x['remote_address'], x['vrf'], x['local_address'])) + + def remove_default_entries(self, data): + + profiles = data.get('profiles', None) + single_hops = data.get('single_hops', None) + multi_hops = data.get('multi_hops', None) + + if profiles: + for profile in profiles: + enabled = profile.get('enabled', None) + transmit_interval = profile.get('transmit_interval', None) + receive_interval = profile.get('receive_interval', None) + detect_multiplier = profile.get('detect_multiplier', None) + passive_mode = profile.get('passive_mode', None) + min_ttl = profile.get('min_ttl', None) + echo_interval = profile.get('echo_interval', None) + echo_mode = profile.get('echo_mode', None) + + if enabled: + profile.pop('enabled') + if transmit_interval == 300: + profile.pop('transmit_interval') + if receive_interval == 300: + profile.pop('receive_interval') + if detect_multiplier == 3: + profile.pop('detect_multiplier') + if passive_mode is False: + profile.pop('passive_mode') + if min_ttl == 254: + profile.pop('min_ttl') + if echo_interval == 300: + profile.pop('echo_interval') + if echo_mode is False: + profile.pop('echo_mode') + + if single_hops: + for hop in single_hops: + enabled = hop.get('enabled', None) + transmit_interval = hop.get('transmit_interval', None) + receive_interval = hop.get('receive_interval', None) + detect_multiplier = hop.get('detect_multiplier', None) + passive_mode = hop.get('passive_mode', None) + echo_interval = hop.get('echo_interval', None) + echo_mode = hop.get('echo_mode', None) + + if enabled: + hop.pop('enabled') + if transmit_interval == 300: + hop.pop('transmit_interval') + if receive_interval == 300: + hop.pop('receive_interval') + if detect_multiplier == 3: + hop.pop('detect_multiplier') + if passive_mode is False: + hop.pop('passive_mode') + if echo_interval == 300: + hop.pop('echo_interval') + if echo_mode is False: + hop.pop('echo_mode') + + if multi_hops: + for hop in multi_hops: + enabled = hop.get('enabled', None) + transmit_interval = hop.get('transmit_interval', None) + receive_interval = hop.get('receive_interval', None) + detect_multiplier = hop.get('detect_multiplier', None) + passive_mode = hop.get('passive_mode', None) + min_ttl = hop.get('min_ttl', None) + + if enabled: + hop.pop('enabled') + if transmit_interval == 300: + hop.pop('transmit_interval') + if receive_interval == 300: + hop.pop('receive_interval') + if detect_multiplier == 3: + hop.pop('detect_multiplier') + if passive_mode is False: + hop.pop('passive_mode') + if min_ttl == 254: + hop.pop('min_ttl') 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 index fd4d5c57e..69c7ca455 100644 --- 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 @@ -13,18 +13,12 @@ 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 ( + remove_empties, 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 ( @@ -32,10 +26,8 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s 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 @@ -120,6 +112,11 @@ class Bgp(ConfigBase): to the desired configuration """ want = self._module.params['config'] + if want: + want = [remove_empties(conf) for conf in want] + else: + want = [] + have = existing_bgp_facts resp = self.set_state(want, have) return to_list(resp) @@ -137,19 +134,85 @@ class Bgp(ConfigBase): requests = [] state = self._module.params['state'] - diff = get_diff(want, have, TEST_KEYS) - if state == 'overridden': - commands, requests = self._state_overridden(want, have, diff) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': - commands, requests = self._state_merged(want, have, diff) + commands, requests = self._state_merged(want, have) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) return commands, requests - def _state_merged(self, want, have, diff): + def _state_replaced(self, want, have): + """ The command generator when state is replaced + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'replaced') + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests + + add_commands = get_diff(want, have, TEST_KEYS) + if add_commands: + for command in add_commands: + as_val = command['bgp_as'] + vrf_name = command['vrf_name'] + + # max_med -> on_startup options are modified or deleted at once. + # Diff might not reflect the correct commands if only one of + # them is modified. So, update the command with want value. + if command.get('max_med'): + for cfg in want: + if cfg['vrf_name'] == vrf_name and cfg['bgp_as'] == as_val: + command['max_med'] = cfg['max_med'] + break + + commands.extend(update_states(add_commands, 'replaced')) + requests.extend(self.get_modify_bgp_requests(add_commands, have)) + + return commands, requests + + def _state_overridden(self, want, have): + """ 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 = [] + + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'overridden') + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests + + add_commands = get_diff(want, have, TEST_KEYS) + if add_commands: + for command in add_commands: + as_val = command['bgp_as'] + vrf_name = command['vrf_name'] + + # max_med -> on_startup options are modified or deleted at once. + # Diff will not reflect the correct commands if only one of + # them is modified. So, update the command with want value. + if command.get('max_med'): + for cfg in want: + if cfg['vrf_name'] == vrf_name and cfg['bgp_as'] == as_val: + command['max_med'] = cfg['max_med'] + break + + commands.extend(update_states(add_commands, 'overridden')) + requests.extend(self.get_modify_bgp_requests(add_commands, 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 @@ -158,7 +221,7 @@ class Bgp(ConfigBase): :returns: the commands necessary to merge the provided into the current configuration """ - commands = diff + commands = get_diff(want, have, TEST_KEYS) requests = self.get_modify_bgp_requests(commands, have) if commands and len(requests) > 0: commands = update_states(commands, "merged") @@ -167,7 +230,7 @@ class Bgp(ConfigBase): return commands, requests - def _state_deleted(self, want, have, diff): + def _state_deleted(self, want, have): """ The command generator when state is deleted :param want: the objects from which the configuration should be removed @@ -269,6 +332,7 @@ class Bgp(ConfigBase): requests = [] router_id = command.get('router_id', None) + rt_delay = command.get('rt_delay', None) timers = command.get('timers', None) holdtime = None keepalive = None @@ -282,6 +346,10 @@ class Bgp(ConfigBase): 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 rt_delay and match.get('rt_delay', None): + url = '%s=%s/%s/global/config/route-map-process-delay' % (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}) @@ -320,7 +388,7 @@ class Bgp(ConfigBase): 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): + if cmd.get('router_id', None) or cmd.get('log_neighbor_changes', None) or cmd.get('bestpath', None) or cmd.get('rt_delay', None): requests.extend(self.get_delete_specific_bgp_param_request(cmd, match)) else: # delete entire bgp @@ -471,7 +539,7 @@ class Bgp(ConfigBase): payload = {} if holdtime is not None: - payload['hold-time'] = str(holdtime) + payload['hold-time'] = holdtime if payload: url = '%s=%s/%s/global/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.holdtime_path) @@ -485,7 +553,7 @@ class Bgp(ConfigBase): payload = {} if keepalive_interval is not None: - payload['keepalive-interval'] = str(keepalive_interval) + payload['keepalive-interval'] = keepalive_interval if payload: url = '%s=%s/%s/global/%s' % (self.network_instance_path, vrf_name, self.protocol_bgp_path, self.keepalive_path) @@ -514,7 +582,7 @@ class Bgp(ConfigBase): return request - def get_modify_global_config_request(self, vrf_name, router_id, as_val): + def get_modify_global_config_request(self, vrf_name, router_id, as_val, rt_delay): request = None method = PATCH payload = {} @@ -524,6 +592,8 @@ class Bgp(ConfigBase): cfg['router-id'] = router_id if as_val: cfg['as'] = float(as_val) + if rt_delay: + cfg['route-map-process-delay'] = rt_delay if cfg: payload['openconfig-network-instance:config'] = cfg @@ -547,6 +617,7 @@ class Bgp(ConfigBase): max_med = None holdtime = None keepalive_interval = None + rt_delay = None if 'bgp_as' in conf: as_val = conf['bgp_as'] @@ -558,6 +629,8 @@ class Bgp(ConfigBase): bestpath = conf['bestpath'] if 'max_med' in conf: max_med = conf['max_med'] + if 'rt_delay' in conf: + rt_delay = conf['rt_delay'] if 'timers' in conf and conf['timers']: if 'holdtime' in conf['timers']: holdtime = conf['timers']['holdtime'] @@ -569,7 +642,7 @@ class Bgp(ConfigBase): if new_bgp_req: requests.append(new_bgp_req) - global_req = self.get_modify_global_config_request(vrf_name, router_id, as_val) + global_req = self.get_modify_global_config_request(vrf_name, router_id, as_val, rt_delay) if global_req: requests.append(global_req) @@ -596,3 +669,112 @@ class Bgp(ConfigBase): requests.extend(max_med_reqs) return requests + + def get_delete_commands_requests_for_replaced_overridden(self, want, have, state): + """Returns the commands and requests necessary to remove applicable + current configurations when state is replaced or overridden + """ + commands = [] + requests = [] + if not have: + return commands, requests + + for conf in have: + as_val = conf['bgp_as'] + vrf_name = conf['vrf_name'] + + match_cfg = next((cfg for cfg in want if cfg['vrf_name'] == vrf_name and cfg['bgp_as'] == as_val), None) + # Delete entire BGP if not specified in overridden + if not match_cfg: + if state == 'overridden': + commands.append(conf) + requests.append(self.get_delete_single_bgp_request(vrf_name)) + continue + + # Delete config in BGP AS that are replaced/overridden + # - Modified attributes are not deleted, since they will be + # updated by merge. + # - log_neighbor_changes is enabled by default, therefore + # it will be enabled if not specified and currently + # disabled for an existing BGP AS. + command = {} + + if conf.get('router_id') and not match_cfg.get('router_id'): + command['router_id'] = conf['router_id'] + + if conf.get('rt_delay') and match_cfg.get('rt_delay') is None: + command['rt_delay'] = conf['rt_delay'] + + if not conf.get('log_neighbor_changes') and match_cfg.get('log_neighbor_changes') is None: + command['log_neighbor_changes'] = False + requests.append(self.get_modify_log_change_request(vrf_name, True)) + + # max_med -> on_startup options are deleted at once. + # Update the commands appropriately. + if conf.get('max_med') and (not match_cfg.get('max_med') or conf['max_med']['on_startup'] != match_cfg['max_med']['on_startup']): + command['max_med'] = conf['max_med'] + + if conf.get('timers'): + timer_command = {} + timers = conf['timers'] + match_timers = match_cfg.get('timers', {}) + if timers.get('holdtime') is not None and match_timers.get('holdtime') is None and timers['holdtime'] != 180: + timer_command['holdtime'] = timers['holdtime'] + if timers.get('keepalive_interval') is not None and match_timers.get('keepalive_interval') is None and timers['keepalive_interval'] != 60: + timer_command['keepalive_interval'] = timers['keepalive_interval'] + + if timer_command: + command['timers'] = timer_command + + if conf.get('bestpath'): + bestpath_command = {} + bestpath = conf['bestpath'] + match_bestpath = match_cfg.get('bestpath', {}) + if bestpath.get('as_path'): + as_path_command = {} + as_path = bestpath['as_path'] + match_as_path = match_bestpath.get('as_path', {}) + for option in ('confed', 'ignore', 'multipath_relax', 'multipath_relax_as_set'): + if as_path.get(option) and match_as_path.get(option) is None: + as_path_command[option] = True + + if as_path_command: + bestpath_command['as_path'] = as_path_command + + if bestpath.get('compare_routerid') and match_bestpath.get('compare_routerid') is None: + bestpath_command['compare_routerid'] = True + + if bestpath.get('med'): + med_command = {} + med = bestpath['med'] + match_med = match_bestpath.get('med', {}) + for option in ('confed', 'missing_as_worst', 'always_compare_med'): + if med.get(option) and match_med.get(option) is None: + med_command[option] = True + + if med_command: + bestpath_command['med'] = med_command + + if bestpath_command: + command['bestpath'] = bestpath_command + + if command: + command['bgp_as'] = as_val + command['vrf_name'] = vrf_name + commands.append(command) + requests.extend(self.get_delete_specific_bgp_param_request(command, command)) + + 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 commands, 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 index 2a5c4cfca..05d74fff2 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2019 Red Hat +# Copyright 2023 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) """ @@ -14,17 +14,16 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type try: - from urllib import quote + from urllib import quote_plus except ImportError: - from urllib.parse import quote + from urllib.parse import quote_plus from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( - ConfigBase, + ConfigBase ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_empties, 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 ( @@ -32,14 +31,11 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s 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, + 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.utils.bgp_utils import ( - validate_bgps, + validate_bgps ) from ansible.module_utils.connection import ConnectionError @@ -49,7 +45,8 @@ TEST_KEYS = [ {'config': {'vrf_name': '', 'bgp_as': ''}}, {'afis': {'afi': '', 'safi': ''}}, {'redistribute': {'protocol': ''}}, - {'route_advertise_list': {'advertise_afi': ''}} + {'route_advertise_list': {'advertise_afi': ''}}, + {'vnis': {'vni_number': ''}} ] @@ -71,9 +68,31 @@ class Bgp_af(ConfigBase): 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' + l2vpn_evpn_vnis_path = 'l2vpn-evpn/openconfig-bgp-evpn-ext:vnis' afi_safi_path = 'global/afi-safis/afi-safi' table_connection_path = 'table-connections/table-connection' + advertise_attrs_map = { + 'advertise_pip': 'advertise-pip', + 'advertise_pip_ip': 'advertise-pip-ip', + 'advertise_pip_peer_ip': 'advertise-pip-peer-ip', + 'advertise_svi_ip': 'advertise-svi-ip', + 'advertise_default_gw': 'advertise-default-gw', + 'advertise_all_vni': 'advertise-all-vni', + 'rd': 'route-distinguisher', + 'rt_in': 'import-rts', + 'rt_out': 'export-rts' + } + non_list_advertise_attrs = ( + 'advertise_pip', + 'advertise_pip_ip', + 'advertise_pip_peer_ip', + 'advertise_svi_ip', + 'advertise_default_gw', + 'advertise_all_vni', + 'rd' + ) + def __init__(self, module): super(Bgp_af, self).__init__(module) @@ -125,7 +144,15 @@ class Bgp_af(ConfigBase): :returns: the commands necessary to migrate the current configuration to the desired configuration """ + state = self._module.params['state'] want = self._module.params['config'] + if want: + # In state deleted, specific empty parameters are supported + if state != 'deleted': + want = [remove_empties(conf) for conf in want] + else: + want = [] + have = existing_bgp_af_facts resp = self.set_state(want, have) return to_list(resp) @@ -143,19 +170,64 @@ class Bgp_af(ConfigBase): requests = [] state = self._module.params['state'] - diff = get_diff(want, have, TEST_KEYS) - if state == 'overridden': - commands, requests = self._state_overridden(want, have, diff) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': - commands, requests = self._state_merged(want, have, diff) + commands, requests = self._state_merged(want, have) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) + return commands, requests - def _state_merged(self, want, have, diff): + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + validate_bgps(self._module, want, have) + + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'replaced') + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests + + add_commands = get_diff(want, have, TEST_KEYS) + if add_commands: + commands.extend(update_states(add_commands, 'replaced')) + requests.extend(self.get_modify_bgp_af_requests(add_commands, have)) + + return commands, requests + + def _state_overridden(self, want, have): + """ 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 = [] + validate_bgps(self._module, want, have) + + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'overridden') + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests + + add_commands = get_diff(want, have, TEST_KEYS) + if add_commands: + commands.extend(update_states(add_commands, 'overridden')) + requests.extend(self.get_modify_bgp_af_requests(add_commands, 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 @@ -164,7 +236,7 @@ class Bgp_af(ConfigBase): :returns: the commands necessary to merge the provided into the current configuration """ - commands = diff + commands = get_diff(want, have, TEST_KEYS) validate_bgps(self._module, commands, have) requests = self.get_modify_bgp_af_requests(commands, have) if commands and len(requests) > 0: @@ -173,7 +245,7 @@ class Bgp_af(ConfigBase): commands = [] return commands, requests - def _state_deleted(self, want, have, diff): + def _state_deleted(self, want, have): """ The command generator when state is deleted :param want: the objects from which the configuration should be removed @@ -191,7 +263,6 @@ class Bgp_af(ConfigBase): 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") @@ -208,7 +279,7 @@ class Bgp_af(ConfigBase): return ({"path": url, "method": PATCH, "data": pay_load}) - def get_modify_advertise_request(self, vrf_name, conf_afi, conf_safi, conf_addr_fam): + def get_modify_evpn_adv_cfg_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) @@ -216,26 +287,30 @@ class Bgp_af(ConfigBase): 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_rd = conf_addr_fam.get('rd', None) + conf_rt_in = conf_addr_fam.get('rt_in', []) + conf_rt_out = conf_addr_fam.get('rt_out', []) afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() evpn_cfg = {} - if conf_adv_pip: + if conf_adv_pip is not None: 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: + if conf_adv_svi_ip is not None: evpn_cfg['advertise-svi-ip'] = conf_adv_svi_ip - - if conf_adv_all_vni: + if conf_adv_all_vni is not None: evpn_cfg['advertise-all-vni'] = conf_adv_all_vni - - if conf_adv_default_gw: + if conf_adv_default_gw is not None: evpn_cfg['advertise-default-gw'] = conf_adv_default_gw + if conf_rd: + evpn_cfg['route-distinguisher'] = conf_rd + if conf_rt_in: + evpn_cfg['import-rts'] = conf_rt_in + if conf_rt_out: + evpn_cfg['export-rts'] = conf_rt_out if evpn_cfg: url = '%s=%s/%s/global' % (self.network_instance_path, vrf_name, self.protocol_bgp_path) @@ -247,6 +322,52 @@ class Bgp_af(ConfigBase): return request + def get_modify_evpn_vnis_request(self, vrf_name, conf_afi, conf_safi, conf_addr_fam): + request = None + conf_vnis = conf_addr_fam.get('vnis', []) + afi_safi = ("%s_%s" % (conf_afi, conf_safi)).upper() + vnis_dict = {} + vni_list = [] + + if conf_vnis: + for vni in conf_vnis: + vni_dict = {} + cfg = {} + vni_number = vni.get('vni_number', None) + adv_default_gw = vni.get('advertise_default_gw', None) + adv_svi_ip = vni.get('advertise_svi_ip', None) + rd = vni.get('rd', None) + rt_in = vni.get('rt_in', []) + rt_out = vni.get('rt_out', []) + + if vni_number: + cfg['vni-number'] = vni_number + if adv_default_gw is not None: + cfg['advertise-default-gw'] = adv_default_gw + if adv_svi_ip is not None: + cfg['advertise-svi-ip'] = adv_svi_ip + if rd: + cfg['route-distinguisher'] = rd + if rt_in: + cfg['import-rts'] = rt_in + if rt_out: + cfg['export-rts'] = rt_out + if cfg: + vni_dict['config'] = cfg + vni_dict['vni-number'] = vni_number + vni_list.append(vni_dict) + + if vni_list: + vnis_dict['vni'] = vni_list + 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:vnis': vnis_dict} + 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 = [] @@ -259,7 +380,7 @@ class Bgp_af(ConfigBase): 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) + url += '/%s=%s/%s/route-advertise-list' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_route_advertise_path) cfg = None if route_map: route_map_list = [route_map] @@ -267,7 +388,7 @@ class Bgp_af(ConfigBase): 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}} + pay_load = {'openconfig-bgp-evpn-ext:route-advertise-list': route_advertise} request = {"path": url, "method": PATCH, "data": pay_load} return request @@ -373,9 +494,15 @@ class Bgp_af(ConfigBase): 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) + cfg_req = self.get_modify_evpn_adv_cfg_request(vrf_name, conf_afi, conf_safi, conf_addr_fam) + vni_req = self.get_modify_evpn_vnis_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 cfg_req: + requests.append(cfg_req) + if vni_req: + requests.append(vni_req) + if rt_adv_req: + requests.append(rt_adv_req) return requests def get_modify_all_af_requests(self, conf_addr_fams, vrf_name): @@ -418,16 +545,19 @@ class Bgp_af(ConfigBase): if conf_afi == 'ipv4' and conf_safi == 'unicast': conf_dampening = conf_addr_fam.get('dampening', None) - if conf_dampening: + if conf_dampening is not None: 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) + cfg_req = self.get_modify_evpn_adv_cfg_request(vrf_name, conf_afi, conf_safi, conf_addr_fam) + vni_req = self.get_modify_evpn_vnis_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 cfg_req: + requests.append(cfg_req) + if vni_req: + requests.append(vni_req) if rt_adv_req: requests.append(rt_adv_req) @@ -451,13 +581,14 @@ class Bgp_af(ConfigBase): 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: + if 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)) + + # 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: + 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_redistribute_route_map_request(vrf_name, conf_afi, have_redis, have_route_map)) modify_redis = {} if conf_metric is not None: @@ -465,7 +596,7 @@ class Bgp_af(ConfigBase): if conf_route_map: modify_redis['route_map'] = conf_route_map - if modify_redis: + if modify_redis or have_redis is None: modify_redis['protocol'] = conf_redis['protocol'] modify_redis_arr.append(modify_redis) @@ -531,50 +662,100 @@ class Bgp_af(ConfigBase): return ({'path': url, 'method': DELETE}) - def get_delete_route_advertise_requests(self, commands, have, is_delete_all): + def get_delete_all_vnis_request(self, vrf_name, conf_afi, conf_safi, conf_vnis): + requests = [] + for vni in conf_vnis: + requests.append(self.get_delete_vni_request(vrf_name, conf_afi, conf_safi, vni['vni_number'])) + + return requests + + def get_delete_vni_request(self, vrf_name, conf_afi, conf_safi, vni_number): + 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/vni=%s' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_vnis_path, vni_number) + + return ({'path': url, 'method': DELETE}) + + def get_delete_vni_cfg_attr_request(self, vrf_name, conf_afi, conf_safi, vni_number, 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/vni=%s' % (self.afi_safi_path, afi_safi, self.l2vpn_evpn_vnis_path, vni_number) + url += '/config/%s' % attr + + return ({'path': url, 'method': DELETE}) + + def get_delete_rt(self, conf_rt, mat_rt): + del_rt_list = [] + for rt in conf_rt: + if mat_rt and rt in mat_rt: + del_rt_list.append(rt) + encoded_del_rt_list = quote_plus(','.join(del_rt_list)) + + return encoded_del_rt_list + + def get_delete_route_advertise_requests(self, vrf_name, conf_afi, conf_safi, conf_route_adv_list, is_delete_all, mat_route_adv_list): 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)) + if is_delete_all: + requests.append(self.get_delete_route_advertise_request(vrf_name, conf_afi, conf_safi)) + else: + for conf_rt_adv in conf_route_adv_list: + conf_advertise_afi = conf_rt_adv.get('advertise_afi', None) + conf_route_map = conf_rt_adv.get('route_map', None) + # Check if the commands to be deleted are configured + for mat_rt_adv in mat_route_adv_list: + mat_advertise_afi = mat_rt_adv.get('advertise_afi', None) + mat_route_map = mat_rt_adv.get('route_map', None) + # Deletion at advertise-afi-safi level + if (not conf_route_map and conf_advertise_afi == mat_advertise_afi): + requests.append(self.get_delete_route_advertise_list_request(vrf_name, conf_afi, conf_safi, conf_advertise_afi)) + # Deletion at route-map level + if (conf_route_map and conf_advertise_afi == mat_advertise_afi and conf_route_map == mat_route_map): + requests.append(self.get_delete_route_advertise_route_map_request(vrf_name, conf_afi, conf_safi, conf_advertise_afi, conf_route_map)) + + return requests + + def get_delete_vnis_requests(self, vrf_name, conf_afi, conf_safi, conf_vnis, is_delete_all, mat_vnis): + requests = [] + if is_delete_all: + requests.extend(self.get_delete_all_vnis_request(vrf_name, conf_afi, conf_safi, conf_vnis)) + else: + for conf_vni in conf_vnis: + conf_vni_number = conf_vni.get('vni_number', None) + conf_adv_default_gw = conf_vni.get('advertise_default_gw', None) + conf_adv_svi_ip = conf_vni.get('advertise_svi_ip', None) + conf_rd = conf_vni.get('rd', None) + conf_rt_in = conf_vni.get('rt_in', None) + conf_rt_out = conf_vni.get('rt_out', None) + # Check if the commands to be deleted are configured + for mat_vni in mat_vnis: + mat_vni_number = mat_vni.get('vni_number', None) + mat_adv_default_gw = mat_vni.get('advertise_default_gw', None) + mat_adv_svi_ip = mat_vni.get('advertise_svi_ip', None) + mat_rd = mat_vni.get('rd', None) + mat_rt_in = mat_vni.get('rt_in', None) + mat_rt_out = mat_vni.get('rt_out', None) + # Deletion at vni-number level + if (conf_vni_number and conf_vni_number == mat_vni_number and not conf_adv_default_gw and not conf_adv_svi_ip and not conf_rd and not + conf_rt_in and not conf_rt_out): + requests.append(self.get_delete_vni_request(vrf_name, conf_afi, conf_safi, conf_vni_number)) + # Deletion at config/attribute level + if conf_vni_number == mat_vni_number: + if conf_adv_default_gw is not None and conf_adv_default_gw == mat_adv_default_gw: + requests.append(self.get_delete_vni_cfg_attr_request(vrf_name, conf_afi, conf_safi, conf_vni_number, 'advertise-default-gw')) + if conf_adv_svi_ip is not None and conf_adv_svi_ip == mat_adv_svi_ip: + requests.append(self.get_delete_vni_cfg_attr_request(vrf_name, conf_afi, conf_safi, conf_vni_number, 'advertise-svi-ip')) + if conf_rd and conf_rd == mat_rd: + requests.append(self.get_delete_vni_cfg_attr_request(vrf_name, conf_afi, conf_safi, conf_vni_number, 'route-distinguisher')) + if conf_rt_in: + del_rt_list = self.get_delete_rt(conf_rt_in, mat_rt_in) + if del_rt_list: + requests.append(self.get_delete_vni_cfg_attr_request(vrf_name, conf_afi, conf_safi, conf_vni_number, 'import-rts=%s' % + del_rt_list)) + if conf_rt_out: + del_rt_list = self.get_delete_rt(conf_rt_out, mat_rt_out) + if del_rt_list: + requests.append(self.get_delete_vni_cfg_attr_request(vrf_name, conf_afi, conf_safi, conf_vni_number, 'export-rts=%s' % + del_rt_list)) return requests @@ -588,11 +769,10 @@ class Bgp_af(ConfigBase): 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} + 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 @@ -630,27 +810,42 @@ class Bgp_af(ConfigBase): 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', []) + conf_route_adv_list = conf_addr_fam.get('route_advertise_list', []) + conf_rd = conf_addr_fam.get('rd', None) + conf_rt_in = conf_addr_fam.get('rt_in', []) + conf_rt_out = conf_addr_fam.get('rt_out', []) + conf_vnis = conf_addr_fam.get('vnis', []) 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: + if conf_adv_pip is not None: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip')) + if conf_adv_svi_ip is not None: requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-svi-ip')) - if conf_adv_all_vni: + if conf_adv_all_vni is not None: 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: + if conf_adv_default_gw is not None: requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-default-gw')) + if conf_route_adv_list: + requests.extend(self.get_delete_route_advertise_requests(vrf_name, conf_afi, conf_safi, conf_route_adv_list, is_delete_all, None)) + if conf_rd: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'route-distinguisher')) + if conf_rt_in: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'import-rts')) + if conf_rt_out: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'export-rts')) 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)) + if conf_vnis: + requests.extend(self.get_delete_vnis_requests(vrf_name, conf_afi, conf_safi, conf_vnis, 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) @@ -674,54 +869,87 @@ class Bgp_af(ConfigBase): 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')) + mat_route_adv_list = match_addr_fam.get('route_advertise_list', None) + mat_rd = match_addr_fam.get('rd', None) + mat_rt_in = match_addr_fam.get('rt_in', []) + mat_rt_out = match_addr_fam.get('rt_out', []) + mat_vnis = match_addr_fam.get('vnis', []) + + if (conf_adv_pip is None and not conf_adv_pip_ip and not conf_adv_pip_peer_ip 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 and not conf_route_adv_list and not conf_rd and not conf_rt_in and not conf_rt_out and not conf_vnis): 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: + if mat_advt_pip is not None: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip')) + if mat_advt_svi_ip is not None: 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: + if mat_dampening: requests.append(self.get_delete_dampening_request(vrf_name, conf_afi, conf_safi)) - if mat_advt_defaut_gw: + if mat_advt_defaut_gw is not None: requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-default-gw')) + if mat_route_adv_list: + requests.extend(self.get_delete_route_advertise_requests(vrf_name, conf_afi, conf_safi, mat_route_adv_list, is_delete_all, + mat_route_adv_list)) + if mat_rd: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'route-distinguisher')) + if mat_rt_in: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'import-rts')) + if mat_rt_out: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'export-rts')) 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)) + if mat_vnis: + requests.extend(self.get_delete_vnis_requests(vrf_name, conf_afi, conf_safi, mat_vnis, is_delete_all, mat_vnis)) 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: + if conf_adv_pip_ip and conf_adv_pip_ip == 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: + if conf_adv_pip_peer_ip and conf_adv_pip_peer_ip == 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: + if conf_adv_pip is not None and conf_adv_pip == mat_advt_pip: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-pip')) + if conf_adv_svi_ip is not None and conf_adv_svi_ip == 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: + if conf_adv_all_vni is not None and conf_adv_all_vni == 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: + if conf_dampening and conf_dampening == 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: + if conf_adv_default_gw is not None and conf_adv_default_gw == mat_advt_defaut_gw: requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'advertise-default-gw')) + if conf_route_adv_list and mat_route_adv_list: + requests.extend(self.get_delete_route_advertise_requests(vrf_name, conf_afi, conf_safi, conf_route_adv_list, is_delete_all, + mat_route_adv_list)) + if conf_rd and conf_rd == mat_rd: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'route-distinguisher')) + if conf_rt_in: + del_rt_list = self.get_delete_rt(conf_rt_in, mat_rt_in) + if del_rt_list: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'import-rts=%s' % + del_rt_list)) + if conf_rt_out: + del_rt_list = self.get_delete_rt(conf_rt_out, mat_rt_out) + if del_rt_list: + requests.append(self.get_delete_advertise_attribute_request(vrf_name, conf_afi, conf_safi, 'export-rts=%s' % + del_rt_list)) 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)) + if conf_vnis and mat_vnis: + requests.extend(self.get_delete_vnis_requests(vrf_name, conf_afi, conf_safi, conf_vnis, is_delete_all, mat_vnis)) break return requests @@ -761,14 +989,14 @@ class Bgp_af(ConfigBase): 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}) + if (conf_ebgp and mat_ebgp and mat_ebgp != 1) or (is_delete_all and conf_ebgp != 1): + requests.append({'path': url + 'ebgp/config/maximum-paths', 'method': DELETE}) + if (conf_ibgp and mat_ibgp and mat_ibgp != 1) or (is_delete_all and conf_ibgp != 1): + requests.append({'path': url + 'ibgp/config/maximum-paths', 'method': DELETE}) return requests - def get_delete_route_map_request(self, vrf_name, conf_afi, conf_redis, conf_route_map): + def get_delete_redistribute_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': @@ -779,6 +1007,17 @@ class Bgp_af(ConfigBase): 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_metric_request(self, vrf_name, conf_afi, conf_redis): + 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/metric' % (src_protocol, dst_protocol, addr_family) + 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: @@ -846,3 +1085,190 @@ class Bgp_af(ConfigBase): 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 + + def get_delete_commands_requests_for_replaced_overridden(self, want, have, state): + """Returns the commands and requests necessary to remove applicable + current configurations when state is replaced or overridden + """ + commands = [] + requests = [] + if not have: + return commands, requests + + for conf in have: + as_val = conf['bgp_as'] + vrf_name = conf['vrf_name'] + if conf.get('address_family') and conf['address_family'].get('afis'): + afi_list = conf['address_family']['afis'] + else: + continue + + match_cfg = next((cfg for cfg in want if cfg['vrf_name'] == vrf_name and cfg['bgp_as'] == as_val), None) + if not match_cfg: + # Delete all address-families in BGPs that are not + # specified in overridden + if state == 'overridden': + commands.append(conf) + requests.extend(self.get_delete_single_bgp_af_request(conf, True)) + continue + + match_afi_list = [] + if match_cfg.get('address_family') and match_cfg['address_family'].get('afis'): + match_afi_list = match_cfg['address_family']['afis'] + + # Delete AF configs in BGPs that are replaced/overridden + afi_command_list = [] + for afi_conf in afi_list: + afi_command = {} + afi = afi_conf['afi'] + safi = afi_conf['safi'] + + match_afi_cfg = next((afi_cfg for afi_cfg in match_afi_list if afi_cfg['afi'] == afi and afi_cfg['safi'] == safi), None) + # Delete address-families that are not specified + if not match_afi_cfg: + afi_command_list.append(afi_conf) + requests.extend(self.get_delete_single_bgp_af_request({'bgp_as': as_val, 'vrf_name': vrf_name, 'address_family': {'afis': [afi_conf]}}, + True)) + continue + + if afi == 'ipv4' and safi == 'unicast': + if afi_conf.get('dampening') and match_afi_cfg.get('dampening') is None: + afi_command['dampening'] = afi_conf['dampening'] + requests.append(self.get_delete_dampening_request(vrf_name, afi, safi)) + + if afi == 'l2vpn' and safi == 'evpn': + for option in self.non_list_advertise_attrs: + if afi_conf.get(option) is not None and match_afi_cfg.get(option) is None: + afi_command[option] = afi_conf[option] + requests.append(self.get_delete_advertise_attribute_request(vrf_name, afi, safi, self.advertise_attrs_map[option])) + + for option in ('rt_in', 'rt_out'): + if afi_conf.get(option): + del_rt = self._get_diff_list(afi_conf[option], match_afi_cfg.get(option, [])) + if del_rt: + afi_command[option] = del_rt + requests.append(self.get_delete_advertise_attribute_request(vrf_name, afi, safi, + '{0}={1}'.format(self.advertise_attrs_map[option], + quote_plus(','.join(del_rt))))) + + if afi_conf.get('route_advertise_list'): + route_adv_list = [] + match_route_adv_list = match_afi_cfg.get('route_advertise_list', []) + for route_adv in afi_conf['route_advertise_list']: + advertise_afi = route_adv['advertise_afi'] + route_map = route_adv.get('route_map') + match_route_adv = next((adv_cfg for adv_cfg in match_route_adv_list if adv_cfg['advertise_afi'] == advertise_afi), None) + if not match_route_adv: + route_adv_list.append(route_adv) + requests.append(self.get_delete_route_advertise_list_request(vrf_name, afi, safi, advertise_afi)) + # Delete existing route-map before configuring + # new route-map. + elif route_map and route_map != match_route_adv.get('route_map'): + route_adv_list.append(route_adv) + requests.append(self.get_delete_route_advertise_route_map_request(vrf_name, afi, safi, advertise_afi, route_map)) + + if route_adv_list: + afi_command['route_advertise_list'] = route_adv_list + + if afi_conf.get('vnis'): + vni_command_list = [] + match_vni_list = match_afi_cfg.get('vnis', []) + for vni_conf in afi_conf['vnis']: + vni_number = vni_conf['vni_number'] + match_vni = next((vni_cfg for vni_cfg in match_vni_list if vni_cfg['vni_number'] == vni_number), None) + # Delete entire VNIs that are not specified + if not match_vni: + vni_command_list.append(vni_conf) + requests.append(self.get_delete_vni_request(vrf_name, afi, safi, vni_number)) + else: + vni_command = {} + for option in ('advertise_default_gw', 'advertise_svi_ip', 'rd'): + if vni_conf.get(option) is not None and match_vni.get(option) is None: + vni_command[option] = vni_conf[option] + requests.append(self.get_delete_vni_cfg_attr_request(vrf_name, afi, safi, vni_number, + self.advertise_attrs_map[option])) + + for option in ('rt_in', 'rt_out'): + if vni_conf.get(option): + del_rt = self._get_diff_list(vni_conf[option], match_vni.get(option, [])) + if del_rt: + vni_command[option] = del_rt + requests.append(self.get_delete_vni_cfg_attr_request(vrf_name, afi, safi, vni_number, + '{0}={1}'.format(self.advertise_attrs_map[option], + quote_plus(','.join(del_rt))))) + + if vni_command: + vni_command['vni_number'] = vni_number + vni_command_list.append(vni_command) + + if vni_command_list: + afi_command['vnis'] = vni_command_list + + elif afi in ['ipv4', 'ipv6'] and safi == 'unicast': + if afi_conf.get('network'): + del_network = self._get_diff_list(afi_conf['network'], match_afi_cfg.get('network', [])) + if del_network: + afi_command['network'] = del_network + requests.extend(self.get_delete_network_request(vrf_name, afi, safi, del_network, True, None)) + + if afi_conf.get('redistribute'): + match_redis_list = match_afi_cfg.get('redistribute') + if not match_redis_list: + afi_command['redistribute'] = afi_conf['redistribute'] + requests.extend(self.get_delete_redistribute_requests(vrf_name, afi, safi, afi_conf['redistribute'], True, None)) + else: + redis_command_list = [] + for redis_conf in afi_conf['redistribute']: + protocol = redis_conf['protocol'] + match_redis = next((redis_cfg for redis_cfg in match_redis_list if redis_cfg['protocol'] == protocol), None) + # Delete complete protocol redistribute + # configuration if not specified + if not match_redis: + redis_command_list.append(redis_conf) + requests.extend(self.get_delete_redistribute_requests(vrf_name, afi, safi, [redis_conf], True, None)) + # Delete metric, route_map for specified + # protocol if they are not specified. + else: + redis_command = {} + if redis_conf.get('metric') is not None and match_redis.get('metric') is None: + redis_command['metric'] = redis_conf['metric'] + requests.append(self.get_delete_redistribute_metric_request(vrf_name, afi, redis_conf)) + if redis_conf.get('route_map') is not None and match_redis.get('route_map') is None: + redis_command['route_map'] = redis_conf['route_map'] + requests.append(self.get_delete_redistribute_route_map_request(vrf_name, afi, redis_conf, redis_command['route_map'])) + + if redis_command: + redis_command['protocol'] = protocol + redis_command_list.append(redis_command) + + if redis_command_list: + afi_command['redistribute'] = redis_command_list + + if afi_conf.get('max_path'): + max_path_command = {} + match_max_path = match_afi_cfg.get('max_path', {}) + if afi_conf['max_path'].get('ibgp') and afi_conf['max_path']['ibgp'] != 1 and match_max_path.get('ibgp') is None: + max_path_command['ibgp'] = afi_conf['max_path']['ibgp'] + if afi_conf['max_path'].get('ebgp') and afi_conf['max_path']['ebgp'] != 1 and match_max_path.get('ebgp') is None: + max_path_command['ebgp'] = afi_conf['max_path']['ebgp'] + + if max_path_command: + afi_command['max_path'] = max_path_command + requests.extend(self.get_delete_max_path_requests(vrf_name, afi, safi, afi_command['max_path'], False, afi_command['max_path'])) + + if afi_command: + afi_command['afi'] = afi + afi_command['safi'] = safi + afi_command_list.append(afi_command) + + if afi_command_list: + commands.append({'bgp_as': as_val, 'vrf_name': vrf_name, 'address_family': {'afis': afi_command_list}}) + + return commands, requests + + @staticmethod + def _get_diff_list(base_list, compare_with_list): + if not compare_with_list: + return base_list + + return [item for item in base_list if item not in compare_with_list] 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 index dc2b023b1..d57cf36e2 100644 --- 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 @@ -17,13 +17,13 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.c ) 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.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 @@ -120,50 +120,142 @@ class Bgp_as_paths(ConfigBase): 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) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': + diff = get_diff(want, have) commands, requests = self._state_merged(want, have, diff) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) return commands, requests - @staticmethod - def _state_replaced(**kwargs): + def _state_replaced(self, want, have): """ The command generator when state is replaced :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ + add_commands = [] + del_commands = [] commands = [] - return commands + requests = [] + + for cmd in want: + # Set action to deny if not specfied for as-path-list + if cmd.get('permit') is None: + cmd['permit'] = False + + match = search_obj_in_list(cmd['name'], have, 'name') + # Replace existing as-path-list + if match: + # Delete entire as-path-list if no members are specified + if not cmd.get('members'): + del_commands.append(match) + requests.append(self.get_delete_single_as_path_request(cmd['name'])) + else: + if cmd['permit'] != match['permit']: + # If action is changed, delete the entire as-path list + # and add the given configuration + del_commands.append(match) + requests.append(self.get_delete_single_as_path_request(cmd['name'])) + add_commands.append(cmd) + requests.append(self.get_new_add_request(cmd)) + else: + want_members_set = set(cmd['members']) + have_members_set = set(match['members']) + members_to_delete = list(have_members_set.difference(want_members_set)) + members_to_add = list(want_members_set.difference(have_members_set)) + if members_to_delete: + del_commands.append({'name': cmd['name'], 'permit': cmd['permit'], 'members': members_to_delete}) + if len(members_to_delete) == len(match['members']): + requests.append(self.get_delete_single_as_path_request(cmd['name'])) + else: + requests.append(self.get_delete_single_as_path_member_request(cmd['name'], members_to_delete)) + + if members_to_add: + add_commands.append({'name': cmd['name'], 'permit': cmd['permit'], 'members': members_to_add}) + requests.append(self.get_new_add_request({'name': cmd['name'], 'permit': cmd['permit'], 'members': members_to_add})) + else: + if cmd.get('members'): + add_commands.append(cmd) + requests.append(self.get_new_add_request(cmd)) + + if del_commands: + commands = update_states(del_commands, 'deleted') + + if add_commands: + commands.extend(update_states(add_commands, 'replaced')) + + return commands, requests - @staticmethod - def _state_overridden(**kwargs): + def _state_overridden(self, want, have): """ The command generator when state is overridden :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ + add_commands = [] + del_commands = [] commands = [] - return commands + requests = [] + + # Delete as-path-lists that are not specified + for cfg in have: + if not search_obj_in_list(cfg['name'], want, 'name'): + del_commands.append(cfg) + requests.append(self.get_delete_single_as_path_request(cfg['name'])) + + for cmd in want: + # Set action to deny if not specfied for as-path-list + if cmd.get('permit') is None: + cmd['permit'] = False + + match = search_obj_in_list(cmd['name'], have, 'name') + # Override existing as-path-list + if match: + # Delete entire as-path-list if no members are specified + if not cmd.get('members'): + del_commands.append(match) + requests.append(self.get_delete_single_as_path_request(cmd['name'])) + else: + if cmd['permit'] != match['permit']: + # If action is changed, delete the entire as-path list + # and add the given configuration + del_commands.append(match) + requests.append(self.get_delete_single_as_path_request(cmd['name'])) + add_commands.append(cmd) + requests.append(self.get_new_add_request(cmd)) + else: + want_members_set = set(cmd['members']) + have_members_set = set(match['members']) + members_to_delete = list(have_members_set.difference(want_members_set)) + members_to_add = list(want_members_set.difference(have_members_set)) + if members_to_delete: + del_commands.append({'name': cmd['name'], 'permit': cmd['permit'], 'members': members_to_delete}) + if len(members_to_delete) == len(match['members']): + requests.append(self.get_delete_single_as_path_request(cmd['name'])) + else: + requests.append(self.get_delete_single_as_path_member_request(cmd['name'], members_to_delete)) + + if members_to_add: + add_commands.append({'name': cmd['name'], 'permit': cmd['permit'], 'members': members_to_add}) + requests.append(self.get_new_add_request({'name': cmd['name'], 'permit': cmd['permit'], 'members': members_to_add})) + else: + if cmd.get('members'): + add_commands.append(cmd) + requests.append(self.get_new_add_request(cmd)) + + if del_commands: + commands = update_states(del_commands, 'deleted') + + if add_commands: + commands.extend(update_states(add_commands, 'overridden')) + + return commands, requests def _state_merged(self, want, have, diff): """ The command generator when state is merged @@ -173,6 +265,19 @@ class Bgp_as_paths(ConfigBase): the current configuration """ commands = diff + for cmd in commands: + match = next((item for item in have if item['name'] == cmd['name']), None) + if match: + # Use existing action if not specified + if cmd.get('permit') is None: + cmd['permit'] = match['permit'] + elif cmd['permit'] != match['permit']: + action = 'permit' if match['permit'] else 'deny' + self._module.fail_json(msg='Cannot override existing action {0} of {1}'.format(action, cmd['name'])) + # Set action to deny if not specfied for a new as-path-list + elif cmd.get('permit') is None: + cmd['permit'] = False + requests = self.get_modify_as_path_list_requests(commands, have) if commands and len(requests) > 0: commands = update_states(commands, "merged") @@ -181,7 +286,7 @@ class Bgp_as_paths(ConfigBase): return commands, requests - def _state_deleted(self, want, have, diff): + def _state_deleted(self, want, have): """ The command generator when state is deleted :rtype: A list @@ -239,7 +344,7 @@ class Bgp_as_paths(ConfigBase): requests.append(request) return requests - def get_delete_single_as_path_member_requests(self, name, members): + def get_delete_single_as_path_member_request(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" @@ -248,15 +353,8 @@ class Bgp_as_paths(ConfigBase): 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): + def get_delete_single_as_path_request(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 @@ -270,25 +368,18 @@ class Bgp_as_paths(ConfigBase): 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)) + match = next((item for item in have if item['name'] == cmd['name']), None) + if match: + if members: + if match.get('members'): + del_members = set(match['members']).intersection(set(members)) + if del_members: + if len(del_members) == len(match['members']): + requests.append(self.get_delete_single_as_path_request(name)) + else: + requests.append(self.get_delete_single_as_path_member_request(name, del_members)) + else: + requests.append(self.get_delete_single_as_path_request(name)) return requests 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 index 670fb26d3..82ed70a3f 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -62,6 +62,13 @@ class Bgp_communities(ConfigBase): 'bgp_communities', ] + standard_communities_map = { + 'no_peer': 'NOPEER', + 'no_export': 'NO_EXPORT', + 'no_advertise': 'NO_ADVERTISE', + 'local_as': 'NO_EXPORT_SUBCONFED' + } + def __init__(self, module): super(Bgp_communities, self).__init__(module) @@ -89,6 +96,7 @@ class Bgp_communities(ConfigBase): 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: @@ -116,6 +124,13 @@ class Bgp_communities(ConfigBase): to the desired configuration """ want = self._module.params['config'] + if want: + for conf in want: + if conf.get("match", None): + conf["match"] = conf["match"].upper() + if conf.get("members", {}) and conf['members'].get("regex", []): + conf['members']['regex'].sort() + have = existing_bgp_communities_facts resp = self.set_state(want, have) return to_list(resp) @@ -138,17 +153,16 @@ class Bgp_communities(ConfigBase): # 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) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': commands, requests = self._state_merged(want, have, diff) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) return commands, requests - @staticmethod - def _state_replaced(**kwargs): + def _state_replaced(self, want, have): """ The command generator when state is replaced :rtype: A list @@ -156,10 +170,13 @@ class Bgp_communities(ConfigBase): to the desired configuration """ commands = [] - return commands + requests = [] + + commands, requests = self.get_replaced_overridden_config(want, have, "replaced") - @staticmethod - def _state_overridden(**kwargs): + return commands, requests + + def _state_overridden(self, want, have): """ The command generator when state is overridden :rtype: A list @@ -167,7 +184,11 @@ class Bgp_communities(ConfigBase): to the desired configuration """ commands = [] - return commands + requests = [] + + commands, requests = self.get_replaced_overridden_config(want, have, "overridden") + + return commands, requests def _state_merged(self, want, have, diff): """ The command generator when state is merged @@ -177,7 +198,7 @@ class Bgp_communities(ConfigBase): the current configuration """ commands = diff - requests = self.get_modify_bgp_community_requests(commands, have) + requests = self.get_modify_bgp_community_requests(commands, have, "merged") if commands and len(requests) > 0: commands = update_states(commands, "merged") else: @@ -185,7 +206,7 @@ class Bgp_communities(ConfigBase): return commands, requests - def _state_deleted(self, want, have, diff): + def _state_deleted(self, want, have): """ The command generator when state is deleted :rtype: A list @@ -217,28 +238,18 @@ class Bgp_communities(ConfigBase): return commands, requests - def get_delete_single_bgp_community_member_requests(self, name, type, members): + def get_delete_single_bgp_community_member_requests(self, name, 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_params = {'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_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" @@ -255,70 +266,90 @@ class Bgp_communities(ConfigBase): 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: + members = cmd.get('members', None) + cmd_type = cmd['type'] + diff_members = [] + + for item in have: + if item['name'] == name: + if 'permit' not in cmd or cmd['permit'] is None: + cmd['permit'] = item['permit'] + + if cmd == item: requests.append(self.get_delete_single_bgp_community_requests(name)) + break + + if cmd_type == "standard": + for attr in self.standard_communities_map: + if cmd.get(attr, None) and item[attr] and cmd[attr] == item[attr]: + diff_members.append(self.standard_communities_map[attr]) + + if members: + if members.get('regex', []): + for member_want in members['regex']: + if item.get('members', None) and item['members'].get('regex', []): + if str(member_want) in item['members']['regex']: + diff_members.append("REGEX:" + str(member_want)) + else: + requests.append(self.get_delete_single_bgp_community_requests(name)) + + else: + if cmd_type == "standard": + no_attr = True + for attr in self.standard_communities_map: + if cmd.get(attr, None): + no_attr = False + break + if no_attr: + requests.append(self.get_delete_single_bgp_community_requests(name)) + else: + requests.append(self.get_delete_single_bgp_community_requests(name)) + break + + if diff_members: + requests.extend(self.get_delete_single_bgp_community_member_requests(name, diff_members)) - # 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)) + community_members = [] + community_action = "" 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['type'] == 'standard': + for attr in self.standard_communities_map: + if attr in conf and conf[attr]: + community_members.append(self.standard_communities_map[attr]) + if 'members' in conf and conf['members'] and conf['members'].get('regex', []): + for i in conf['members']['regex']: + community_members.extend([str(i)]) + if not community_members: + self._module.fail_json(msg='Cannot create standard community-list {0} without community attributes'.format(conf['name'])) + + elif conf['type'] == 'expanded': + if 'members' in conf and conf['members'] and conf['members'].get('regex', []): + for i in conf['members']['regex']: + community_members.extend(["REGEX:" + str(i)]) + if not community_members: + self._module.fail_json(msg='Cannot create expanded community-list {0} without community attributes'.format(conf['name'])) + if conf['permit']: - input_data['permit'] = "PERMIT" + community_action = "PERMIT" else: - input_data['permit'] = "DENY" + community_action = "DENY" + + input_data = {'name': conf['name'], 'members_list': community_members, 'match': conf['match'].upper(), 'permit': community_action} + payload_template = """ { "openconfig-bgp-policy:community-sets": { @@ -328,7 +359,7 @@ class Bgp_communities(ConfigBase): "config": { "community-set-name": "{{name}}", "community-member": [ - {% for member in members_list %}"{{regex}}{{member}}"{%- if not loop.last -%},{% endif %}{%endfor%} + {% for member in members_list %}"{{member}}"{%- if not loop.last -%},{% endif %}{%endfor%} ], "openconfig-bgp-policy-ext:action": "{{permit}}", "match-set-options": "{{match}}" @@ -342,27 +373,118 @@ class Bgp_communities(ConfigBase): 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): + def get_modify_bgp_community_requests(self, commands, have, cur_state): 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'] + if cur_state == "merged": + for item in have: + if item['name'] == conf['name']: + if 'type' not in conf: + conf['type'] = item['type'] + if 'permit' not in conf or conf['permit'] is None: + conf['permit'] = item['permit'] + if 'match' not in conf: + conf['match'] = item['match'] + if conf['type'] == "standard": + for attr in self.standard_communities_map: + if attr not in conf and attr in item: + conf[attr] = item[attr] + else: + if 'members' not in conf: + if item.get('members', {}) and item['members'].get('regex', []): + conf['members'] = {'regex': item['members']['regex']} + else: + conf['members'] = item['members'] + break + new_req = self.get_new_add_request(conf) if new_req: requests.append(new_req) return requests + + def get_replaced_overridden_config(self, want, have, cur_state): + commands, requests = [], [] + + commands_del, requests_del = [], [] + commands_add, requests_add = [], [] + + for conf in want: + name = conf['name'] + in_have = False + for have_conf in have: + if have_conf['name'] == name: + in_have = True + if have_conf['type'] != conf['type']: + # If both community list are of same name but different types + commands_del.append(have_conf) + commands_add.append(conf) + else: + is_change = False + + if have_conf['permit'] != conf['permit']: + is_change = True + + if have_conf['match'] != conf['match']: + is_change = is_delete = True + + if conf["type"] == "standard": + no_attr = True + for attr in self.standard_communities_map: + if not conf.get(attr, None): + if have_conf.get(attr, None): + is_change = True + else: + no_attr = False + if not have_conf.get(attr, None): + is_change = True + + if no_attr: + # Since standard type needs atleast one attribute to exist + self._module.fail_json(msg='Cannot create standard community-list {0} without community attributes'.format(conf['name'])) + else: + members = conf.get('members', {}) + if members and members.get('regex', []): + if have_conf.get('members', {}) and have_conf['members'].get('regex', []): + if set(have_conf['members']['regex']).symmetric_difference(set(members['regex'])): + is_change = True + else: + # If there are no members in any community list of want, then + # that particular community list request to be ignored since + # expanded type needs community-member to exist + self._module.fail_json(msg='Cannot create expanded community-list {0} without community attributes'.format(conf['name'])) + + if is_change: + commands_add.append(conf) + commands_del.append(have_conf) + break + + if not in_have: + commands_add.append(conf) + + if cur_state == "overridden": + for have_conf in have: + in_want = next((conf for conf in want if conf['name'] == have_conf['name']), None) + if not in_want: + commands_del.append(have_conf) + + if commands_del: + requests_del = self.get_delete_bgp_communities(commands_del, have, False) + + if len(requests_del) > 0: + commands.extend(update_states(commands_del, "deleted")) + requests.extend(requests_del) + + if commands_add: + requests_add = self.get_modify_bgp_community_requests(commands_add, have, cur_state) + + if len(requests_add) > 0: + commands.extend(update_states(commands_add, cur_state)) + requests.extend(requests_add) + + return commands, requests 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 index 751f88e48..8cd9953e6 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -60,6 +60,11 @@ class Bgp_ext_communities(ConfigBase): 'bgp_ext_communities', ] + standard_communities_map = { + "route_origin": "route-origin", + "route_target": "route-target" + } + def __init__(self, module): super(Bgp_ext_communities, self).__init__(module) @@ -87,6 +92,7 @@ class Bgp_ext_communities(ConfigBase): 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: @@ -114,6 +120,21 @@ class Bgp_ext_communities(ConfigBase): to the desired configuration """ want = self._module.params['config'] + if want: + for conf in want: + cmd_type = conf.get("type", None) + if cmd_type and conf.get("match", None): + conf['match'] = conf['match'].lower() + if cmd_type and conf.get("members", {}): + if cmd_type == "expanded": + if conf['members'].get("regex", []): + conf['members']['regex'].sort() + else: + if conf['members'].get("route_origin", []): + conf['members']['route_origin'].sort() + if conf['members'].get("route_target", []): + conf['members']['route_target'].sort() + have = existing_bgp_ext_communities_facts resp = self.set_state(want, have) return to_list(resp) @@ -133,17 +154,16 @@ class Bgp_ext_communities(ConfigBase): new_want = self.validate_type(want) diff = get_diff(new_want, have) if state == 'overridden': - commands, requests = self._state_overridden(want, have, diff) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': commands, requests = self._state_merged(want, have, diff) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) return commands, requests - @staticmethod - def _state_replaced(**kwargs): + def _state_replaced(self, want, have): """ The command generator when state is replaced :rtype: A list @@ -151,10 +171,13 @@ class Bgp_ext_communities(ConfigBase): to the desired configuration """ commands = [] - return commands + requests = [] + + commands, requests = self.get_replaced_overridden_config(want, have, "replaced") - @staticmethod - def _state_overridden(**kwargs): + return commands, requests + + def _state_overridden(self, want, have): """ The command generator when state is overridden :rtype: A list @@ -162,7 +185,11 @@ class Bgp_ext_communities(ConfigBase): to the desired configuration """ commands = [] - return commands + requests = [] + + commands, requests = self.get_replaced_overridden_config(want, have, "overridden") + + return commands, requests def _state_merged(self, want, have, diff): """ The command generator when state is merged @@ -172,7 +199,7 @@ class Bgp_ext_communities(ConfigBase): the current configuration """ commands = diff - requests = self.get_modify_bgp_ext_community_requests(commands, have) + requests = self.get_modify_bgp_ext_community_requests(commands, have, "merged") if commands and len(requests) > 0: commands = update_states(commands, "merged") else: @@ -180,7 +207,7 @@ class Bgp_ext_communities(ConfigBase): return commands, requests - def _state_deleted(self, want, have, diff): + def _state_deleted(self, want, have): """ The command generator when state is deleted :rtype: A list @@ -204,7 +231,7 @@ class Bgp_ext_communities(ConfigBase): return commands, requests - def get_delete_single_bgp_ext_community_member_requests(self, name, type, members): + def get_delete_single_bgp_ext_community_member_requests(self, name, members): requests = [] for member in members: url = "data/openconfig-routing-policy:routing-policy/defined-sets/openconfig-bgp-policy:" @@ -216,13 +243,6 @@ class Bgp_ext_communities(ConfigBase): 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" @@ -245,71 +265,79 @@ class Bgp_ext_communities(ConfigBase): 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']: + cmd_type = cmd['type'] + members = cmd.get('members', None) + diff_members = [] + + for item in have: + if item["name"] == name: + if 'permit' not in cmd or cmd['permit'] is None: + cmd['permit'] = item['permit'] + if cmd == item: + requests.append(self.get_delete_single_bgp_ext_community_requests(name)) + break + + if members: + if cmd_type == "expanded": + if members.get('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: + if item.get("members", None) and item['members'].get('regex', []): + if str(member_want) in item['members']['regex']: + diff_members.append("REGEX:" + str(member_want)) + else: + requests.append(self.get_delete_single_bgp_ext_community_requests(name)) + else: + no_members = True + for attr in self.standard_communities_map: + if members.get(attr, []): + no_members = False + for member_want in members[attr]: + if item.get("members", None) and item['members'].get(attr, []): + if str(member_want) in item['members'][attr]: + diff_members.append(self.standard_communities_map[attr] + ":" + str(member_want)) + if no_members: + requests.append(self.get_delete_single_bgp_ext_community_requests(name)) + else: requests.append(self.get_delete_single_bgp_ext_community_requests(name)) + break + + if diff_members: + requests.extend(self.get_delete_single_bgp_ext_community_member_requests(name, diff_members)) + 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) + community_members = [] + community_action = "" + 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 'members' in conf and conf['members'] and conf['members'].get('regex', []): + for i in conf['members']['regex']: + community_members.extend(["REGEX:" + str(i)]) + elif conf['type'] == 'standard': + for attr in self.standard_communities_map: + if 'members' in conf and conf['members'] and conf['members'].get(attr, []): + for i in conf['members'][attr]: + community_members.extend([self.standard_communities_map[attr] + ":" + str(i)]) + + if not community_members: + self._module.fail_json(msg='Cannot create {0} community-list {1} without community attributes'.format(conf['type'], conf['name'])) + return {} + if conf['permit']: - input_data['permit'] = "PERMIT" + community_action = "PERMIT" else: - input_data['permit'] = "DENY" + community_action = "DENY" + + input_data = {'name': conf['name'], 'members_list': community_members, 'match': conf['match'].upper(), 'permit': community_action} + payload_template = """ { "openconfig-bgp-policy:ext-community-sets": { @@ -335,23 +363,37 @@ class Bgp_ext_communities(ConfigBase): request = {"path": url, "method": method, "data": ret_payload} return request - def get_modify_bgp_ext_community_requests(self, commands, have): + def get_modify_bgp_ext_community_requests(self, commands, have, cur_state): 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 + if cur_state == "merged": + for item in have: + if item['name'] == conf['name']: + if 'type' not in conf: + conf['type'] = item['type'] + if 'permit' not in conf or conf['permit'] is None: + conf['permit'] = item['permit'] + if 'match' not in conf: + conf['match'] = item['match'] + if 'members' not in conf: + if conf['type'] == "expanded": + if item.get('members', {}) and item['members'].get('regex', []): + conf['members'] = {'regex': item['members']['regex']} + else: + conf['members'] = item['members'] + else: + no_members = True + for attr in self.standard_communities_map: + if item.get('members', {}) and item['members'].get(attr, []): + no_members = False + conf['members'] = {attr: item['members'][attr]} + if no_members: + conf['members'] = item['members'] + break + new_req = self.get_new_add_request(conf) if new_req: requests.append(new_req) @@ -369,3 +411,84 @@ class Bgp_ext_communities(ConfigBase): new_want.append(cfg) return new_want + + def get_replaced_overridden_config(self, want, have, cur_state): + commands, requests = [], [] + + commands_del, requests_del = [], [] + commands_add, requests_add = [], [] + + for conf in want: + name = conf['name'] + in_have = False + for have_conf in have: + if have_conf['name'] == name: + in_have = True + if have_conf['type'] != conf['type']: + # If both extended community list are of same name but different types + commands_del.append(have_conf) + commands_add.append(conf) + else: + is_change = False + + if have_conf['permit'] != conf['permit']: + is_change = True + + if have_conf['match'] != conf['match']: + is_change = True + + if conf["type"] == "expanded": + members = conf.get('members', {}) + if members and conf['members'].get('regex', []): + if have_conf.get('members', {}) and have_conf['members'].get('regex', []): + if set(have_conf['members']['regex']).symmetric_difference(set(members['regex'])): + is_change = True + else: + # If there are no members in any expanded ext community list of want, then + # abort the playbook with an error message explaining why the specified command is not valid + self._module.fail_json(msg='Cannot create expanded extended community-list ' + '{0} without community attributes'.format(conf['name'])) + else: + members = conf.get('members', {}) + no_members = True + for attr in self.standard_communities_map: + if members and conf['members'].get(attr, []): + no_members = False + if have_conf.get('members', {}) and have_conf['members'].get(attr, []): + if set(have_conf['members'][attr]).symmetric_difference(set(members[attr])): + is_change = True + + if no_members: + # If there are no members in any standard ext community list of want, then + # abort the playbook with an error message explaining why the specified command is not valid + self._module.fail_json(msg='Cannot create standard extended community-list ' + '{0} without community attributes'.format(conf['name'])) + + if is_change: + commands_add.append(conf) + commands_del.append(have_conf) + break + if not in_have: + commands_add.append(conf) + + if cur_state == "overridden": + for have_conf in have: + in_want = next((conf for conf in want if conf['name'] == have_conf['name']), None) + if not in_want: + commands_del.append(have_conf) + + if commands_del: + requests_del = self.get_delete_bgp_ext_communities(commands_del, have, False) + + if len(requests_del) > 0: + commands.extend(update_states(commands_del, "deleted")) + requests.extend(requests_del) + + if commands_add: + requests_add = self.get_modify_bgp_ext_community_requests(commands_add, have, cur_state) + + if len(requests_add) > 0: + commands.extend(update_states(commands_add, cur_state)) + requests.extend(requests_add) + + return commands, requests 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 index 31bbec78d..9c0920832 100644 --- 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 @@ -27,6 +27,7 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( update_states, get_diff, + remove_matching_defaults ) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.bgp_utils import ( validate_bgps, @@ -37,6 +38,8 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import to_request from ansible.module_utils.connection import ConnectionError +from copy import deepcopy + PATCH = 'patch' DELETE = 'delete' @@ -47,6 +50,95 @@ TEST_KEYS = [ {'afis': {'afi': '', 'safi': ''}}, ] +default_entries = [ + [ + {'name': 'peer_group'}, + {'name': 'timers'}, + {'name': 'keepalive', 'default': 60} + ], + [ + {'name': 'peer_group'}, + {'name': 'timers'}, + {'name': 'holdtime', 'default': 180} + ], + [ + {'name': 'peer_group'}, + {'name': 'timers'}, + {'name': 'connect_retry', 'default': 30} + ], + [ + {'name': 'peer_group'}, + {'name': 'advertisement_interval', 'default': 30} + ], + [ + {'name': 'peer_group'}, + {'name': 'auth_pwd'}, + {'name': 'encrypted', 'default': False} + ], + [ + {'name': 'peer_group'}, + {'name': 'ebgp_multihop'}, + {'name': 'enabled', 'default': False} + ], + [ + {'name': 'peer_group'}, + {'name': 'passive', 'default': False} + ], + [ + {'name': 'peer_group'}, + {'name': 'address_family'}, + {'name': 'afis'}, + {'name': 'ip_afi'}, + {'name': 'send_default_route', 'default': False} + ], + [ + {'name': 'peer_group'}, + {'name': 'address_family'}, + {'name': 'afis'}, + {'name': 'activate', 'default': False} + ], + [ + {'name': 'peer_group'}, + {'name': 'address_family'}, + {'name': 'afis'}, + {'name': 'prefix_limit'}, + {'name': 'prevent_teardown', 'default': False} + ], + [ + {'name': 'neighbors'}, + {'name': 'timers'}, + {'name': 'keepalive', 'default': 60} + ], + [ + {'name': 'neighbors'}, + {'name': 'timers'}, + {'name': 'holdtime', 'default': 180} + ], + [ + {'name': 'neighbors'}, + {'name': 'timers'}, + {'name': 'connect_retry', 'default': 30} + ], + [ + {'name': 'neighbors'}, + {'name': 'advertisement_interval', 'default': 30} + ], + [ + {'name': 'neighbors'}, + {'name': 'auth_pwd'}, + {'name': 'encrypted', 'default': False} + ], + [ + {'name': 'neighbors'}, + {'name': 'ebgp_multihop'}, + {'name': 'enabled', 'default': False} + ], + [ + {'name': 'neighbors'}, + {'name': 'passive', 'default': False} + ], +] + class Bgp_neighbors(ConfigBase): """ @@ -180,7 +272,9 @@ class Bgp_neighbors(ConfigBase): commands = have new_have = have else: - new_have = self.remove_default_entries(have) + new_have = deepcopy(have) + for default_entry in default_entries: + remove_matching_defaults(new_have, default_entry) 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 @@ -192,141 +286,6 @@ class Bgp_neighbors(ConfigBase): 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 = [] @@ -444,7 +403,7 @@ class Bgp_neighbors(ConfigBase): 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}}}) + self._module.fail_json('Prefix limit configuration not supported for l2vpn evpn') else: if each.get('ip_afi', None) is not None: afi_safi_cfg = get_ip_afi_cfg_payload(each['ip_afi']) @@ -696,13 +655,35 @@ class Bgp_neighbors(ConfigBase): advertisement_interval = each.get('advertisement_interval', None) bfd = each.get('bfd', None) capability = each.get('capability', None) + auth_pwd = each.get('auth_pwd', None) + pg_description = each.get('pg_description', None) + disable_connected_check = each.get('disable_connected_check', None) + dont_negotiate_capability = each.get('dont_negotiate_capability', None) + ebgp_multihop = each.get('ebgp_multihop', None) + enforce_first_as = each.get('enforce_first_as', None) + enforce_multihop = each.get('enforce_multihop', None) + local_address = each.get('local_address', None) + local_as = each.get('local_as', None) + override_capability = each.get('override_capability', None) + passive = each.get('passive', None) + shutdown_msg = each.get('shutdown_msg', None) + solo = each.get('solo', None) + strict_capability_match = each.get('strict_capability_match', None) + ttl_security = each.get('ttl_security', 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: + if (name and not remote_as and not timers and not advertisement_interval and not bfd and not capability and not auth_pwd and not + pg_description and disable_connected_check is None and dont_negotiate_capability is None and not ebgp_multihop and + enforce_first_as is None and enforce_multihop is None and not local_address and not local_as and override_capability + is None and passive is None and not shutdown_msg and solo is None and strict_capability_match is None and not ttl_security 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'] + keys = ['remote_as', 'timers', 'advertisement_interval', 'bfd', 'capability', 'auth_pwd', 'pg_description', + 'disable_connected_check', 'dont_negotiate_capability', 'ebgp_multihop', 'enforce_first_as', 'enforce_multihop', + 'local_address', 'local_as', 'override_capability', 'passive', 'shutdown_msg', 'solo', 'strict_capability_match', + 'ttl_security', '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: @@ -808,7 +789,7 @@ class Bgp_neighbors(ConfigBase): 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' + 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: @@ -857,9 +838,6 @@ class Bgp_neighbors(ConfigBase): 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 @@ -909,12 +887,36 @@ class Bgp_neighbors(ConfigBase): 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: + auth_pwd = each.get('auth_pwd', None) + nbr_description = each.get('nbr_description', None) + disable_connected_check = each.get('disable_connected_check', None) + dont_negotiate_capability = each.get('dont_negotiate_capability', None) + ebgp_multihop = each.get('ebgp_multihop', None) + enforce_first_as = each.get('enforce_first_as', None) + enforce_multihop = each.get('enforce_multihop', None) + local_address = each.get('local_address', None) + local_as = each.get('local_as', None) + override_capability = each.get('override_capability', None) + passive = each.get('passive', None) + port = each.get('port', None) + shutdown_msg = each.get('shutdown_msg', None) + solo = each.get('solo', None) + strict_capability_match = each.get('strict_capability_match', None) + ttl_security = each.get('ttl_security', None) + v6only = each.get('v6only', None) + if (neighbor and not remote_as and not peer_group and not timers and not advertisement_interval and not bfd and not capability and not + auth_pwd and not nbr_description and disable_connected_check is None and dont_negotiate_capability is None and not + ebgp_multihop and enforce_first_as is None and enforce_multihop is None and not local_address and not local_as and + override_capability is None and passive is None and not port and not shutdown_msg and solo is None and strict_capability_match + is None and not ttl_security and v6only is None): 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'] + keys = ['remote_as', 'peer_group', 'timers', 'advertisement_interval', 'bfd', 'capability', 'auth_pwd', 'nbr_description', + 'disable_connected_check', 'dont_negotiate_capability', 'ebgp_multihop', 'enforce_first_as', 'enforce_multihop', + 'local_address', 'local_as', 'override_capability', 'passive', 'port', 'shutdown_msg', 'solo', + 'strict_capability_match', 'ttl_security', 'v6only'] if not any(want_nei_match.get(key, None) for key in keys): requests.append(self.delete_neighbor_whole_request(vrf_name, neighbor)) else: @@ -1034,7 +1036,7 @@ class Bgp_neighbors(ConfigBase): 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' + delete_path = delete_static_path + '/ebgp-multihop/config/multihop-ttl' requests.append({'path': delete_path, 'method': DELETE}) 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 index 15f46f966..196a6eea9 100644 --- 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 @@ -13,11 +13,6 @@ 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, ) @@ -288,7 +283,7 @@ class Bgp_neighbors_af(ConfigBase): 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}} + self._module.fail_json('Prefix limit configuration not supported for l2vpn evpn') else: if conf_ip_afi: ip_afi_cfg = get_ip_afi_cfg_payload(conf_ip_afi) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/copp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/copp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/copp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/copp/copp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/copp/copp.py new file mode 100644 index 000000000..cec802e67 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/copp/copp.py @@ -0,0 +1,393 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_copp 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.utils.utils import ( + get_diff, + get_replaced_config, + send_requests, + remove_empties, + update_states, +) +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 +) + + +COPP_GROUPS_PATH = '/data/openconfig-copp-ext:copp/copp-groups' +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'copp_groups': {'copp_name': ''}} +] +reserved_copp_names = [ + 'copp-system-lacp', + 'copp-system-udld', + 'copp-system-stp', + 'copp-system-bfd', + 'copp-system-ptp', + 'copp-system-lldp', + 'copp-system-vrrp', + 'copp-system-iccp', + 'copp-system-ospf', + 'copp-system-bgp', + 'copp-system-pim', + 'copp-system-igmp', + 'copp-system-suppress', + 'copp-system-arp', + 'copp-system-dhcp', + 'copp-system-icmp', + 'copp-system-ip2me', + 'copp-system-subnet', + 'copp-system-nat', + 'copp-system-mtu', + 'copp-system-sflow', + 'copp-system-default', + 'copp-system-ttl', + 'default' +] + + +class Copp(ConfigBase): + """ + The sonic_copp class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'copp', + ] + + def __init__(self, module): + super(Copp, self).__init__(module) + + def get_copp_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) + copp_facts = facts['ansible_network_resources'].get('copp') + if not copp_facts: + return [] + return copp_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + commands = [] + + existing_copp_facts = self.get_copp_facts() + commands, requests = self.set_config(existing_copp_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_copp_facts = self.get_copp_facts() + + result['before'] = existing_copp_facts + if result['changed']: + result['after'] = changed_copp_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_copp_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_copp_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) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(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 + """ + replaced_config = get_replaced_config(want, have, TEST_KEYS) + + if replaced_config: + is_delete_all = True + requests = self.get_delete_copp_requests(replaced_config, None, is_delete_all) + send_requests(self._module, requests) + + commands = want + else: + commands = diff + + requests = [] + + if commands: + requests = self.get_modify_copp_groups_request(commands) + + if len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + + have = self.filter_copp_groups(have) + if have and have != want: + is_delete_all = True + requests = self.get_delete_copp_requests(have, None, is_delete_all) + send_requests(self._module, requests) + have = [] + + commands = [] + requests = [] + + if not have and want: + commands = want + requests = self.get_modify_copp_groups_request(commands) + + if len(requests) > 0: + commands = update_states(commands, "overridden") + else: + commands = [] + + return 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_copp_groups_request(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 + """ + is_delete_all = False + # if want is none, then delete ALL + want = remove_empties(want) + if not want: + commands = have + is_delete_all = True + else: + commands = want + commands = self.filter_copp_groups(commands) + requests = self.get_delete_copp_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_copp_groups_request(self, commands): + request = None + + copp_groups = commands.get('copp_groups', None) + if copp_groups: + group_list = [] + for group in copp_groups: + config_dict = {} + group_dict = {} + copp_name = group.get('copp_name', None) + trap_priority = group.get('trap_priority', None) + trap_action = group.get('trap_action', None) + queue = group.get('queue', None) + cir = group.get('cir', None) + cbs = group.get('cbs', None) + + if copp_name: + config_dict['name'] = copp_name + group_dict['name'] = copp_name + if trap_priority: + config_dict['trap-priority'] = trap_priority + if trap_action: + config_dict['trap-action'] = trap_action + if queue: + config_dict['queue'] = queue + if cir: + config_dict['cir'] = cir + if cbs: + config_dict['cbs'] = cbs + if config_dict: + group_dict['config'] = config_dict + group_list.append(group_dict) + + if group_list: + copp_groups_dict = {'copp-group': group_list} + payload = {'openconfig-copp-ext:copp-groups': copp_groups_dict} + request = {'path': COPP_GROUPS_PATH, 'method': PATCH, 'data': payload} + + return request + + def get_delete_copp_requests(self, commands, have, is_delete_all): + requests = [] + + if is_delete_all: + copp_groups = commands.get('copp_groups', None) + if copp_groups: + for group in copp_groups: + copp_name = group.get('copp_name', None) + requests.append(self.get_delete_single_copp_group_request(copp_name)) + else: + copp_groups = commands.get('copp_groups', None) + if copp_groups: + for group in copp_groups: + copp_name = group.get('copp_name', None) + trap_priority = group.get('trap_priority', None) + trap_action = group.get('trap_action', None) + queue = group.get('queue', None) + cir = group.get('cir', None) + cbs = group.get('cbs', None) + + if have: + cfg_copp_groups = have.get('copp_groups', None) + if cfg_copp_groups: + for cfg_group in cfg_copp_groups: + cfg_copp_name = cfg_group.get('copp_name', None) + cfg_trap_priority = cfg_group.get('trap_priority', None) + cfg_trap_action = cfg_group.get('trap_action', None) + cfg_queue = cfg_group.get('queue', None) + cfg_cir = cfg_group.get('cir', None) + cfg_cbs = cfg_group.get('cbs', None) + + if copp_name == cfg_copp_name: + if trap_priority and trap_priority == cfg_trap_priority: + requests.append(self.get_delete_copp_groups_attr_request(copp_name, 'trap-priority')) + if trap_action and trap_action == cfg_trap_action: + err_msg = "Deletion of trap-action attribute is not supported." + self._module.fail_json(msg=err_msg, code=405) + requests.append(self.get_delete_copp_groups_attr_request(copp_name, 'trap-action')) + if queue and queue == cfg_queue: + requests.append(self.get_delete_copp_groups_attr_request(copp_name, 'queue')) + if cir and cir == cfg_cir: + requests.append(self.get_delete_copp_groups_attr_request(copp_name, 'cir')) + if cbs and cbs == cfg_cbs: + requests.append(self.get_delete_copp_groups_attr_request(copp_name, 'cbs')) + if not trap_priority and not trap_action and not queue and not cir and not cbs: + requests.append(self.get_delete_single_copp_group_request(copp_name)) + + return requests + + def get_delete_copp_groups_attr_request(self, copp_name, attr): + url = '%s/copp-group=%s/config/%s' % (COPP_GROUPS_PATH, copp_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_single_copp_group_request(self, copp_name): + url = '%s/copp-group=%s' % (COPP_GROUPS_PATH, copp_name) + request = {'path': url, 'method': DELETE} + + return request + + def filter_copp_groups(self, commands): + cfg_dict = {} + + if commands: + copp_groups = commands.get('copp_groups', None) + if copp_groups: + copp_groups_list = [] + for group in copp_groups: + copp_name = group.get('copp_name', None) + if copp_name not in reserved_copp_names: + copp_groups_list.append(group) + if copp_groups_list: + cfg_dict['copp_groups'] = copp_groups_list + + return cfg_dict + + def get_copp_groups_key(self, copp_groups_key): + return copp_groups_key.get('copp_name') + + def sort_lists_in_config(self, config): + if 'copp_groups' in config and config['copp_groups'] is not None: + config['copp_groups'].sort(key=self.get_copp_groups_key) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_relay/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_relay/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_relay/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_relay/dhcp_relay.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_relay/dhcp_relay.py new file mode 100644 index 000000000..64d50fb5b --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_relay/dhcp_relay.py @@ -0,0 +1,695 @@ +# +# -*- 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_dhcp_relay 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, + get_normalize_interface_name, + remove_empties_from_list +) +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' + +DEFAULT_CIRCUIT_ID = '%p' +DEFAULT_MAX_HOP_COUNT = 10 +DEFAULT_POLICY_ACTION = 'discard' + +BOOL_TO_SELECT_VALUE = { + True: 'ENABLE', + False: 'DISABLE' +} + + +class Dhcp_relay(ConfigBase): + """ + The sonic_dhcp_relay class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'dhcp_relay', + ] + + dhcp_relay_intf_path = 'data/openconfig-relay-agent:relay-agent/dhcp/interfaces/interface={intf_name}' + dhcp_relay_intf_config_path = { + 'circuit_id': dhcp_relay_intf_path + '/agent-information-option/config/circuit-id', + 'link_select': dhcp_relay_intf_path + '/agent-information-option/config/openconfig-relay-agent-ext:link-select', + 'max_hop_count': dhcp_relay_intf_path + '/config/openconfig-relay-agent-ext:max-hop-count', + 'policy_action': dhcp_relay_intf_path + '/config/openconfig-relay-agent-ext:policy-action', + 'server_address': dhcp_relay_intf_path + '/config/helper-address={server_address}', + 'server_addresses_all': dhcp_relay_intf_path + '/config/helper-address', + 'source_interface': dhcp_relay_intf_path + '/config/openconfig-relay-agent-ext:src-intf', + 'vrf_name': dhcp_relay_intf_path + '/config/openconfig-relay-agent-ext:vrf', + 'vrf_select': dhcp_relay_intf_path + '/agent-information-option/config/openconfig-relay-agent-ext:vrf-select' + } + + dhcpv6_relay_intf_path = 'data/openconfig-relay-agent:relay-agent/dhcpv6/interfaces/interface={intf_name}' + dhcpv6_relay_intf_config_path = { + 'max_hop_count': dhcpv6_relay_intf_path + '/config/openconfig-relay-agent-ext:max-hop-count', + 'server_address': dhcpv6_relay_intf_path + '/config/helper-address={server_address}', + 'server_addresses_all': dhcpv6_relay_intf_path + '/config/helper-address', + 'source_interface': dhcpv6_relay_intf_path + '/config/openconfig-relay-agent-ext:src-intf', + 'vrf_name': dhcpv6_relay_intf_path + '/config/openconfig-relay-agent-ext:vrf', + 'vrf_select': dhcpv6_relay_intf_path + '/options/config/openconfig-relay-agent-ext:vrf-select' + } + + def __init__(self, module): + super(Dhcp_relay, self).__init__(module) + + def get_dhcp_relay_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) + dhcp_relay_facts = facts['ansible_network_resources'].get('dhcp_relay') + if not dhcp_relay_facts: + return [] + return dhcp_relay_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + + existing_dhcp_relay_facts = self.get_dhcp_relay_facts() + commands, requests = self.set_config(existing_dhcp_relay_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 + + changed_dhcp_relay_facts = self.get_dhcp_relay_facts() + + result['before'] = existing_dhcp_relay_facts + if result['changed']: + result['after'] = changed_dhcp_relay_facts + + result['commands'] = commands + result['warnings'] = warnings + return result + + def set_config(self, existing_dhcp_relay_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 + """ + state = self._module.params['state'] + want = self._module.params['config'] + if want: + # In state deleted, specific empty parameters are supported + if state != 'deleted': + want = remove_empties_from_list(want) + + normalize_interface_name(want, self._module) + for config in want: + if config.get('ipv4') and config['ipv4'].get('source_interface'): + config['ipv4']['source_interface'] = get_normalize_interface_name(config['ipv4']['source_interface'], self._module) + if config.get('ipv6') and config['ipv6'].get('source_interface'): + config['ipv6']['source_interface'] = get_normalize_interface_name(config['ipv6']['source_interface'], self._module) + else: + want = [] + + have = existing_dhcp_relay_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, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) + return commands, requests + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = get_diff(want, have) + requests = self.get_modify_dhcp_dhcpv6_relay_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 = [] + requests = [] + if not want: + commands = have + requests.extend(self.get_delete_dhcp_dhcpv6_relay_completely_requests(commands)) + else: + commands = want + requests.extend(self.get_delete_dhcp_dhcpv6_relay_requests(commands, have)) + + if len(requests) == 0: + commands = [] + + if commands: + commands = update_states(commands, "deleted") + + return commands, requests + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'replaced') + if del_commands: + new_have = get_diff(have, del_commands) + commands = update_states(del_commands, 'deleted') + requests = del_requests + else: + new_have = have + + add_commands = get_diff(want, new_have) + if add_commands: + commands.extend(update_states(add_commands, 'replaced')) + requests.extend(self.get_modify_dhcp_dhcpv6_relay_requests(add_commands)) + + return commands, requests + + def _state_overridden(self, want, have): + """ 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 = [] + + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'overridden') + if del_commands: + new_have = get_diff(have, del_commands) + commands = update_states(del_commands, 'deleted') + requests = del_requests + else: + new_have = have + + add_commands = get_diff(want, new_have) + if add_commands: + commands.extend(update_states(add_commands, 'overridden')) + requests.extend(self.get_modify_dhcp_dhcpv6_relay_requests(add_commands)) + + return commands, requests + + def get_modify_dhcp_dhcpv6_relay_requests(self, commands): + """Get requests to modify DHCP and DHCPv6 relay configurations + for all interfaces specified by the commands + """ + requests = [] + + for command in commands: + if command.get('ipv4'): + requests.extend(self.get_modify_specific_dhcp_relay_param_requests(command)) + if command.get('ipv6'): + requests.extend(self.get_modify_specific_dhcpv6_relay_param_requests(command)) + + return requests + + def get_modify_specific_dhcp_relay_param_requests(self, command): + """Get requests to modify specific DHCP relay configurations + based on the command specified for the interface + """ + requests = [] + + name = command['name'] + ipv4 = command.get('ipv4') + if not ipv4: + return requests + + # Specifying appropriate order for merge to succeed + server_addresses = self.get_server_addresses(ipv4.get('server_addresses')) + if server_addresses: + payload = {'openconfig-relay-agent:helper-address': list(server_addresses)} + url = self.dhcp_relay_intf_config_path['server_addresses_all'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv4.get('vrf_name'): + payload = {'openconfig-relay-agent-ext:vrf': ipv4['vrf_name']} + url = self.dhcp_relay_intf_config_path['vrf_name'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv4.get('source_interface'): + payload = {'openconfig-relay-agent-ext:src-intf': ipv4['source_interface']} + url = self.dhcp_relay_intf_config_path['source_interface'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv4.get('link_select') is not None: + link_select = BOOL_TO_SELECT_VALUE[ipv4['link_select']] + payload = {'openconfig-relay-agent-ext:link-select': link_select} + url = self.dhcp_relay_intf_config_path['link_select'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv4.get('max_hop_count'): + payload = {'openconfig-relay-agent-ext:max-hop-count': ipv4['max_hop_count']} + url = self.dhcp_relay_intf_config_path['max_hop_count'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv4.get('vrf_select') is not None: + vrf_select = BOOL_TO_SELECT_VALUE[ipv4['vrf_select']] + payload = {'openconfig-relay-agent-ext:vrf-select': vrf_select} + url = self.dhcp_relay_intf_config_path['vrf_select'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv4.get('policy_action'): + payload = {'openconfig-relay-agent-ext:policy-action': ipv4['policy_action'].upper()} + url = self.dhcp_relay_intf_config_path['policy_action'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv4.get('circuit_id'): + payload = {'openconfig-relay-agent:circuit-id': ipv4['circuit_id']} + url = self.dhcp_relay_intf_config_path['circuit_id'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + return requests + + def get_modify_specific_dhcpv6_relay_param_requests(self, command): + """Get requests to modify specific DHCPv6 relay configurations + based on the command specified for the interface + """ + requests = [] + + name = command['name'] + ipv6 = command.get('ipv6') + if not ipv6: + return requests + + # Specifying appropriate order for merge to succeed + server_addresses = self.get_server_addresses(ipv6.get('server_addresses')) + if server_addresses: + payload = {'openconfig-relay-agent:helper-address': list(server_addresses)} + url = self.dhcpv6_relay_intf_config_path['server_addresses_all'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv6.get('vrf_name'): + payload = {'openconfig-relay-agent-ext:vrf': ipv6['vrf_name']} + url = self.dhcpv6_relay_intf_config_path['vrf_name'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv6.get('source_interface'): + payload = {'openconfig-relay-agent-ext:src-intf': ipv6['source_interface']} + url = self.dhcpv6_relay_intf_config_path['source_interface'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv6.get('max_hop_count'): + payload = {'openconfig-relay-agent-ext:max-hop-count': ipv6['max_hop_count']} + url = self.dhcpv6_relay_intf_config_path['max_hop_count'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if ipv6.get('vrf_select') is not None: + vrf_select = BOOL_TO_SELECT_VALUE[ipv6['vrf_select']] + payload = {'openconfig-relay-agent-ext:vrf-select': vrf_select} + url = self.dhcpv6_relay_intf_config_path['vrf_select'].format(intf_name=name) + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + return requests + + def get_delete_dhcp_dhcpv6_relay_completely_requests(self, have): + """Get requests to delete all existing DHCP and DHCPv6 relay + configurations in the chassis + """ + requests = [] + for cfg in have: + if cfg.get('ipv4'): + requests.append(self.get_delete_all_dhcp_relay_intf_request(cfg['name'])) + if cfg.get('ipv6'): + requests.append(self.get_delete_all_dhcpv6_relay_intf_request(cfg['name'])) + + return requests + + def get_delete_dhcp_dhcpv6_relay_requests(self, commands, have): + """Get requests to delete DHCP and DHCPv6 relay configurations + based on the commands specified + """ + requests = [] + + for command in commands: + intf_name = command['name'] + have_obj = next((cfg for cfg in have if cfg['name'] == intf_name), None) + if not have_obj: + continue + + have_ipv4 = have_obj.get('ipv4') + have_ipv6 = have_obj.get('ipv6') + + ipv4 = command.get('ipv4') + ipv6 = command.get('ipv6') + if not ipv4 and not ipv6: + if have_ipv4: + requests.append(self.get_delete_all_dhcp_relay_intf_request(intf_name)) + if have_ipv6: + requests.append(self.get_delete_all_dhcpv6_relay_intf_request(intf_name)) + else: + if ipv4 and have_ipv4: + requests.extend(self.get_delete_specific_dhcp_relay_param_requests(command, have_obj)) + if ipv6 and have_ipv6: + requests.extend(self.get_delete_specific_dhcpv6_relay_param_requests(command, have_obj)) + + return requests + + def get_delete_specific_dhcp_relay_param_requests(self, command, config, is_state_deleted=True): + """Get requests to delete specific DHCP relay configurations + based on the command specified for the interface + """ + requests = [] + + name = command['name'] + ipv4 = command.get('ipv4') + have_ipv4 = config.get('ipv4') + if not ipv4 or not have_ipv4: + return requests + + server_addresses = self.get_server_addresses(ipv4.get('server_addresses')) + have_server_addresses = self.get_server_addresses(have_ipv4.get('server_addresses')) + + # Delete all DHCP relay config for an interface, if only + # a single server address with no value is specified. + # + # This "special" YAML sequence is supported to provide + # "delete all AF parameters" functionality despite the Ansible + # infrastructure limitations that prevent use of a simpler + # syntax for deleting an entire AF parameter dictionary. + if (ipv4.get('server_addresses') and len(ipv4.get('server_addresses')) + and not server_addresses): + requests.append(self.get_delete_all_dhcp_relay_intf_request(name)) + return requests + + del_server_addresses = have_server_addresses.intersection(server_addresses) + if del_server_addresses: + # Deleting all DHCP server addresses configured on an + # interface automatically removes all DHCP relay config in + # that interface. Therefore, seperate requests to delete + # other DHCP relay configs are not required. + if is_state_deleted and len(del_server_addresses) == len(have_server_addresses): + requests.append(self.get_delete_all_dhcp_relay_intf_request(name)) + return requests + + for addr in del_server_addresses: + url = self.dhcp_relay_intf_config_path['server_address'].format(intf_name=name, server_address=addr) + requests.append({'path': url, 'method': DELETE}) + + # Specifying appropriate order for deletion to succeed + if ipv4.get('link_select') is not None and have_ipv4.get('link_select'): + url = self.dhcp_relay_intf_config_path['link_select'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + if (ipv4.get('source_interface') and have_ipv4.get('source_interface') + and ipv4['source_interface'] == have_ipv4['source_interface']): + url = self.dhcp_relay_intf_config_path['source_interface'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + if (ipv4.get('max_hop_count') and have_ipv4.get('max_hop_count') + and ipv4['max_hop_count'] == have_ipv4['max_hop_count'] + and have_ipv4['max_hop_count'] != DEFAULT_MAX_HOP_COUNT): + url = self.dhcp_relay_intf_config_path['max_hop_count'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + if ipv4.get('vrf_select') is not None and have_ipv4.get('vrf_select'): + url = self.dhcp_relay_intf_config_path['vrf_select'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + if (ipv4.get('policy_action') and have_ipv4.get('policy_action') + and ipv4['policy_action'] == have_ipv4['policy_action'] + and have_ipv4['policy_action'] != DEFAULT_POLICY_ACTION): + url = self.dhcp_relay_intf_config_path['policy_action'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + if (ipv4.get('circuit_id') and have_ipv4.get('circuit_id') + and ipv4['circuit_id'] == have_ipv4['circuit_id'] + and have_ipv4['circuit_id'] != DEFAULT_CIRCUIT_ID): + url = self.dhcp_relay_intf_config_path['circuit_id'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + return requests + + def get_delete_specific_dhcpv6_relay_param_requests(self, command, have, is_state_deleted=True): + """Get requests to delete specific DHCPv6 relay configurations + based on the command specified for the interface + """ + requests = [] + + name = command['name'] + ipv6 = command.get('ipv6') + have_ipv6 = have.get('ipv6') + if not ipv6 or not have_ipv6: + return requests + + server_addresses = self.get_server_addresses(ipv6.get('server_addresses')) + have_server_addresses = self.get_server_addresses(have_ipv6.get('server_addresses')) + + # Delete all DHCPv6 relay config for an interface, if only + # a single server address with no value is specified. + # + # This "special" YAML sequence is supported to provide + # "delete all AF parameters" functionality despite the Ansible + # infrastructure limitations that prevent use of a simpler + # syntax for deleting an entire AF parameter dictionary. + if (ipv6.get('server_addresses') and len(ipv6.get('server_addresses')) + and not server_addresses): + requests.append(self.get_delete_all_dhcpv6_relay_intf_request(name)) + return requests + + del_server_addresses = have_server_addresses.intersection(server_addresses) + if del_server_addresses: + # Deleting all DHCPv6 server addresses configured on an + # interface automatically removes all DHCPv6 relay config + # in that interface. Therefore, seperate requests to delete + # other DHCPv6 relay configs are not required. + if is_state_deleted and len(del_server_addresses) == len(have_server_addresses): + requests.append(self.get_delete_all_dhcpv6_relay_intf_request(name)) + return requests + + for addr in del_server_addresses: + url = self.dhcpv6_relay_intf_config_path['server_address'].format(intf_name=name, server_address=addr) + requests.append({'path': url, 'method': DELETE}) + + # Specifying appropriate order for deletion to succeed + if (ipv6.get('source_interface') and have_ipv6.get('source_interface') + and ipv6['source_interface'] == have_ipv6['source_interface']): + url = self.dhcpv6_relay_intf_config_path['source_interface'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + if (ipv6.get('max_hop_count') and have_ipv6.get('max_hop_count') + and ipv6['max_hop_count'] == have_ipv6['max_hop_count'] + and have_ipv6['max_hop_count'] != DEFAULT_MAX_HOP_COUNT): + url = self.dhcpv6_relay_intf_config_path['max_hop_count'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + if ipv6.get('vrf_select') is not None and have_ipv6.get('vrf_select'): + url = self.dhcpv6_relay_intf_config_path['vrf_select'].format(intf_name=name) + requests.append({'path': url, 'method': DELETE}) + + return requests + + def get_delete_all_dhcp_relay_intf_request(self, intf_name): + """Get request to delete all DHCP relay configurations in the + specified interface + """ + return {'path': self.dhcp_relay_intf_config_path['server_addresses_all'].format(intf_name=intf_name), 'method': DELETE} + + def get_delete_all_dhcpv6_relay_intf_request(self, intf_name): + """Get request to delete all DHCPv6 relay configurations in the + specified interface + """ + return {'path': self.dhcpv6_relay_intf_config_path['server_addresses_all'].format(intf_name=intf_name), 'method': DELETE} + + def get_delete_commands_requests_for_replaced_overridden(self, want, have, state): + """Returns the commands and requests necessary to remove applicable + current configurations when state is replaced or overridden + """ + default_value = { + 'circuit_id': DEFAULT_CIRCUIT_ID, + 'max_hop_count': DEFAULT_MAX_HOP_COUNT, + 'policy_action': DEFAULT_POLICY_ACTION + } + commands = [] + requests = [] + if not have: + return commands, requests + + for conf in have: + intf_name = conf['name'] + ipv4_conf = conf.get('ipv4') + ipv6_conf = conf.get('ipv6') + + match_obj = next((cmd for cmd in want if cmd['name'] == intf_name), None) + if not match_obj: + # Delete all DHCP and DHCPv6 relay config for interfaces, + # that are not specified in overridden. + if state == 'overridden': + commands.append(conf) + if ipv4_conf: + requests.append(self.get_delete_all_dhcp_relay_intf_request(intf_name)) + if ipv6_conf: + requests.append(self.get_delete_all_dhcpv6_relay_intf_request(intf_name)) + continue + + command = {'name': intf_name} + if ipv4_conf: + match_ipv4 = match_obj.get('ipv4') + # Delete all DHCP relay config for an interface if not specified + if not match_ipv4: + command['ipv4'] = ipv4_conf + requests.append(self.get_delete_all_dhcp_relay_intf_request(intf_name)) + else: + have_server_addresses = self.get_server_addresses(ipv4_conf.get('server_addresses')) + server_addresses = self.get_server_addresses(match_ipv4.get('server_addresses')) + + # Delete all DHCP relay config for an interface, if + # all existing server addresses are to be replaced + # or if the VRF is to be removed. + if (not have_server_addresses.intersection(server_addresses) + or (ipv4_conf.get('vrf_name') and match_ipv4.get('vrf_name') is None)): + command['ipv4'] = ipv4_conf + requests.append(self.get_delete_all_dhcp_relay_intf_request(intf_name)) + else: + ipv4_command = {} + del_server_addresses = have_server_addresses.difference(server_addresses) + if del_server_addresses: + ipv4_command['server_addresses'] = [] + for address in del_server_addresses: + ipv4_command['server_addresses'].append({'address': address}) + + for option in ('source_interface', 'link_select', 'vrf_select'): + if ipv4_conf.get(option) and match_ipv4.get(option) is None: + ipv4_command[option] = ipv4_conf[option] + + for option in ('circuit_id', 'max_hop_count', 'policy_action'): + if (ipv4_conf.get(option) and match_ipv4.get(option) is None + and ipv4_conf[option] != default_value[option]): + ipv4_command[option] = ipv4_conf[option] + + if ipv4_command: + command['ipv4'] = ipv4_command + requests.extend(self.get_delete_specific_dhcp_relay_param_requests(command, command, False)) + + if ipv6_conf: + match_ipv6 = match_obj.get('ipv6') + # Delete all DHCPv6 relay config for an interface if not specified + if not match_ipv6: + command['ipv6'] = ipv6_conf + requests.append(self.get_delete_all_dhcpv6_relay_intf_request(intf_name)) + else: + have_server_addresses = self.get_server_addresses(ipv6_conf.get('server_addresses')) + server_addresses = self.get_server_addresses(match_ipv6.get('server_addresses')) + + # Delete all DHCPv6 relay config for an interface, if + # all existing server addresses are to be replaced + # or if the VRF is to be removed. + if (not have_server_addresses.intersection(server_addresses) + or (ipv6_conf.get('vrf_name') and match_ipv6.get('vrf_name') is None)): + command['ipv6'] = ipv6_conf + requests.append(self.get_delete_all_dhcpv6_relay_intf_request(intf_name)) + else: + ipv6_command = {} + del_server_addresses = have_server_addresses.difference(server_addresses) + if del_server_addresses: + ipv6_command['server_addresses'] = [] + for address in del_server_addresses: + ipv6_command['server_addresses'].append({'address': address}) + + for option in ('source_interface', 'vrf_select'): + if ipv6_conf.get(option) and match_ipv6.get(option) is None: + ipv6_command[option] = ipv6_conf[option] + + if (ipv6_conf.get('max_hop_count') and match_ipv6.get('max_hop_count') is None + and ipv6_conf['max_hop_count'] != default_value['max_hop_count']): + ipv6_command['max_hop_count'] = ipv6_conf['max_hop_count'] + + if ipv6_command: + command['ipv6'] = ipv6_command + requests.extend(self.get_delete_specific_dhcpv6_relay_param_requests(command, command, False)) + + if command.get('ipv4') or command.get('ipv6'): + commands.append(command) + + return commands, requests + + @staticmethod + def get_server_addresses(server_addresses_dict): + """Get a set of server addresses available in the given + server_addresses dict + """ + server_addresses = set() + if not server_addresses_dict: + return server_addresses + + for addr in server_addresses_dict: + if addr.get('address'): + server_addresses.add(addr['address']) + + return server_addresses diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_snooping/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_snooping/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_snooping/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_snooping/dhcp_snooping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_snooping/dhcp_snooping.py new file mode 100644 index 000000000..d3c3233b1 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/dhcp_snooping/dhcp_snooping.py @@ -0,0 +1,649 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_dhcp_snooping 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 copy import deepcopy + +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, + remove_empties, + validate_config +) +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.facts.facts import Facts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states +) + + +class Dhcp_snooping(ConfigBase): + """ + The sonic_dhcp_snooping class + """ + test_keys = [ + {'afis': {'afi': ''}}, + {"source_bindings": {"mac_addr": ""}}, + {"trusted": {"intf_name": ""}} + ] + + ipv4_key = 'ipv4' + ipv6_key = 'ipv6' + + delete_method_value = 'delete' + patch_method_value = 'patch' + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'dhcp_snooping', + ] + + dhcp_snooping_uri = 'data/openconfig-dhcp-snooping:dhcp-snooping' + config_uri = dhcp_snooping_uri + '/config' + enable_uri = config_uri + '/dhcpv{v}-admin-enable' + verify_mac_uri = config_uri + '/dhcpv{v}-verify-mac-address' + binding_uri = dhcp_snooping_uri + '-static-binding/entry' + trusted_uri = 'data/openconfig-interfaces:interfaces/interface={name}/dhcpv{v}-snooping-trust/config/dhcpv{v}-snooping-trust' + vlans_uri = 'data/sonic-vlan:sonic-vlan/VLAN/VLAN_LIST={vlan_name}/dhcpv{v}_snooping_enable' + + def __init__(self, module): + super(Dhcp_snooping, self).__init__(module) + + def get_dhcp_snooping_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) + dhcp_snooping_facts = facts['ansible_network_resources'].get('dhcp_snooping') + if not dhcp_snooping_facts: + return [] + return dhcp_snooping_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_dhcp_snooping_facts = self.get_dhcp_snooping_facts() + commands, requests = self.set_config(existing_dhcp_snooping_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_dhcp_snooping_facts = self.get_dhcp_snooping_facts() + + result['before'] = existing_dhcp_snooping_facts + if result['changed']: + result['after'] = changed_dhcp_snooping_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_dhcp_snooping_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_dhcp_snooping_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 + """ + afis = {} + want = self.remove_none(want) + + # just in case weird arguments passed + if want is None: + want = {} + if have is None: + have = {} + + if want.get('afis') is not None: + for want_afi in want.get('afis'): + if want_afi.get('afi') == self.ipv4_key: + afis['want_ipv4'] = want_afi + elif want_afi.get('afi') == self.ipv6_key: + afis['want_ipv6'] = want_afi + + if have.get('afis') is not None: + for have_afi in have.get('afis'): + if have_afi.get('afi') == self.ipv4_key: + afis['have_ipv4'] = have_afi + elif have_afi.get('afi') == self.ipv6_key: + afis['have_ipv6'] = have_afi + + state = self._module.params['state'] + if state == 'merged': + commands, requests = self._state_merged(want, have, afis) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have, afis) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, afis) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have, afis) + + return commands, requests + + def _state_merged(self, want, have, afis): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + want = remove_empties(want) + self.validate_config({"config": want}) + + commands = get_diff(want, have, test_keys=self.test_keys) + self.prep_replaced_to_merge(commands, afis) + requests = self.get_modify_requests(commands) + + if commands and len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, afis): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + requests = [] + if not have or not have.get('afis'): + # nothing that could be deleted + commands = [] + elif not want or not want.get('afis'): + # want is empty, meaning want to delete all config + # afis parameter only stores the on device config at this point + commands, requests = self.get_delete_all_have_requests(afis) + else: + # some mix of settings specified in both + commands, requests = self.get_delete_specific_requests(afis) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + return commands, requests + + def _state_overridden(self, want, have, afis): + """ 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 = [] + if not want: + return commands, requests + + # Determine if there is any configuration specified in the playbook + # that is not contained in the current configuration. + diff_requested = get_diff(want, have, self.test_keys) + diff_requested_keyed = {} + for afi in diff_requested.get("afis", []): + diff_requested_keyed[afi["afi"]] = afi + + # Determine if there is anything already configured that is not + # specified in the playbook. + diff_unwanted = get_diff(have, want, self.test_keys) + + # Idempotency check: If the configuration already matches the + # requested configuration with no extra attributes, no + # commands should be executed on the device. + if not diff_requested and not diff_unwanted: + return commands, requests + + used_commands_per_afi = [] + commands = [] + + for diff_unwanted_afi in diff_unwanted.get("afis", []): + # enabled and verify_mac can't be deleted from config, only set to default. + # so in the case they appear in both the "need to delete" and "need to change", keeping in both results in double requests + if "enabled" in diff_unwanted_afi and "enabled" in diff_requested_keyed.get(diff_unwanted_afi["afi"], {}): + del diff_unwanted_afi["enabled"] + if "verify_mac" in diff_unwanted_afi and "verify_mac" in diff_requested_keyed.get(diff_unwanted_afi["afi"], {}): + del diff_unwanted_afi["verify_mac"] + afi_commands, afi_requests = self.get_delete_specific_afi_fields_requests(diff_unwanted_afi, afis["have_" + diff_unwanted_afi["afi"]]) + if afi_commands: + afi_commands["afi"] = diff_unwanted_afi["afi"] + used_commands_per_afi.append(afi_commands) + requests.extend(afi_requests) + if len(used_commands_per_afi): + commands = {"afis": used_commands_per_afi} + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + + # apply the things to add or change + # need to add back in the source bindings since the diff could pick up only the different values in a source binding + self.prep_replaced_to_merge(diff_requested, afis) + overridden_requests = self.get_modify_requests(diff_requested) + requests.extend(overridden_requests) + if diff_requested and len(overridden_requests) > 0: + diff_requested = update_states(diff_requested, "overridden") + commands.extend(diff_requested) + return commands, requests + + def _state_replaced(self, want, have, afis): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + # do needed deletes + commands, requests = self.get_delete_replaced_groupings(afis) + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + # getting what needs to be added/changed after deletes + # need to add back in the source bindings since the diff could pick up only the different values in a source binding + diff = get_diff(want, have, self.test_keys) + self.prep_replaced_to_merge(diff, afis) + merged_commands = diff + + replaced_requests = self.get_modify_requests(merged_commands) + requests.extend(replaced_requests) + if merged_commands and len(replaced_requests) > 0: + merged_commands = update_states(merged_commands, "replaced") + commands.extend(merged_commands) + return commands, requests + + def validate_config(self, config): + '''validate passed in config is argspec compliant. Also does checks on values in ranges that ansible might not do''' + validated_config = validate_config(self._module.argument_spec, config) + return validated_config + + def remove_none(self, config): + '''goes through nested dictionary items and removes any keys that have None as value. + enables using empty list/dict to specify clear everything for that section and differentiate this + 'clear everything' case from when no value was given + remove_empties in ansible utils will remove empty lists and dicts as well as None''' + if isinstance(config, dict): + for k, v in list(config.items()): + if v is None: + del config[k] + else: + self.remove_none(v) + elif isinstance(config, list): + for item in list(config): + if item is None: + config.remove(item) + self.remove_none(item) + return config + + def get_modify_requests(self, to_modify_config): + '''builds and returns requests to add in given config + + :param to_modify: dictionary specifying what to modify in argspec format. expected to be at root level of config''' + requests = [] + + if to_modify_config.get('afis') is not None: + for afi_config in to_modify_config.get('afis'): + requests.extend(self.get_single_afi_modify_requests(afi_config)) + + return requests + + def get_single_afi_modify_requests(self, to_modify_afi): + """build requests to modify a single afi family. Uses passed in config to find which family and what to change + + :param to_modify_afi: dictionary specifying the config to add/change in argspec format. expected to be for a single afi + :param v: version number of afi to modify + """ + requests = [] + v = self.afi_to_vnum(to_modify_afi) + + if to_modify_afi.get('enabled') is not None: + payload = {'openconfig-dhcp-snooping:dhcpv{v}-admin-enable'.format(v=v): to_modify_afi['enabled']} + uri = self.enable_uri.format(v=v) + requests.append({'path': uri, 'method': self.patch_method_value, 'data': payload}) + + if to_modify_afi.get('verify_mac') is not None: + payload = {'openconfig-dhcp-snooping:dhcpv{v}-verify-mac-address'.format(v=v): to_modify_afi['verify_mac']} + uri = self.verify_mac_uri.format(v=v) + requests.append({'path': uri, 'method': self.patch_method_value, 'data': payload}) + + if to_modify_afi.get('trusted'): + for intf in to_modify_afi.get('trusted'): + intf_name = intf.get("intf_name") + if intf_name: + payload = {'openconfig-interfaces:dhcpv{v}-snooping-trust'.format(v=v): 'ENABLE'} + uri = self.trusted_uri.format(name=intf_name, v=v) + requests.append({'path': uri, 'method': self.patch_method_value, 'data': payload}) + + if to_modify_afi.get('vlans'): + for vlan_id in to_modify_afi.get('vlans'): + payload = {'sonic-vlan:dhcpv{v}_snooping_enable'.format(v=v): 'enable'} + uri = self.vlans_uri.format(vlan_name='Vlan' + vlan_id, v=v) + requests.append({'path': uri, 'method': self.patch_method_value, 'data': payload}) + + if to_modify_afi.get('source_bindings'): + entries = [] + for entry in to_modify_afi.get('source_bindings'): + if entry.get('mac_addr'): + entries.append({ + 'mac': entry.get('mac_addr'), + 'iptype': 'ipv' + str(v), + 'config': { + 'mac': entry.get('mac_addr'), + 'iptype': 'ipv' + str(v), + 'vlan': "Vlan" + str(entry.get('vlan_id')), + 'interface': entry.get('intf_name'), + 'ip': entry.get('ip_addr'), + } + }) + + payload = {'openconfig-dhcp-snooping:entry': entries} + uri = self.binding_uri + requests.append({'path': uri, 'method': self.patch_method_value, 'data': payload}) + + return requests + + def get_delete_all_have_requests(self, afis): + '''creates and builds list of requests to delete all current dhcp snooping config for ipv4 and ipv6''' + modified_afi_commands = [] + requests = [] + ipv4_commands, ipv4_requests = self.get_delete_specific_afi_fields_requests(afis.get('have_ipv4'), afis.get('have_ipv4')) + requests.extend(ipv4_requests) + if ipv4_commands: + ipv4_commands["afi"] = afis.get('have_ipv4')["afi"] + modified_afi_commands.append(ipv4_commands) + ipv6_commands, ipv6_requests = self.get_delete_specific_afi_fields_requests(afis.get('have_ipv6'), afis.get('have_ipv6')) + requests.extend(ipv6_requests) + if ipv6_commands: + ipv6_commands["afi"] = afis.get('have_ipv6')["afi"] + modified_afi_commands.append(ipv6_commands) + + sent_commands = [] + if modified_afi_commands: + sent_commands = {"afis": modified_afi_commands} + + return sent_commands, requests + + def get_delete_specific_requests(self, afis): + '''creates and returns list of requests to delete afi settings. + Checks if clearing settings for a ip family or just matching fields in config''' + modified_afi_commands = [] + requests = [] + + want_ipv4 = afis.get('want_ipv4') + want_ipv6 = afis.get('want_ipv6') + have_ipv4 = afis.get('have_ipv4') + have_ipv6 = afis.get('have_ipv6') + + if want_ipv4: + if want_ipv4.keys() == set(["afi"]): + # just afi key supplied, interpreting this as delete all config for that afi + ipv4_commands, ipv4_requests = self.get_delete_specific_afi_fields_requests(have_ipv4, have_ipv4) + else: + ipv4_commands, ipv4_requests = self.get_delete_specific_afi_fields_requests(want_ipv4, have_ipv4) + requests.extend(ipv4_requests) + if ipv4_commands: + ipv4_commands["afi"] = want_ipv4["afi"] + modified_afi_commands.append(ipv4_commands) + if want_ipv6: + if want_ipv6.keys() == set(["afi"]): + ipv6_commands, ipv6_requests = self.get_delete_specific_afi_fields_requests(have_ipv6, have_ipv6) + else: + ipv6_commands, ipv6_requests = self.get_delete_specific_afi_fields_requests(want_ipv6, have_ipv6) + requests.extend(ipv6_requests) + if ipv6_commands: + ipv6_commands["afi"] = want_ipv6["afi"] + modified_afi_commands.append(ipv6_commands) + + sent_commands = [] + if modified_afi_commands: + sent_commands = {"afis": modified_afi_commands} + + return sent_commands, requests + + def get_delete_specific_afi_fields_requests(self, want_afi, have_afi): + '''creates and builds list of requests for deleting some fields of dhcp snooping config for + one ip family. Each field checked and deleted independently from each other depending on if + it is specified in playbook and matches with current config''' + sent_commands = {} + requests = [] + + if want_afi.get('enabled') is True and have_afi.get('enabled') is True: + # only need to send a request if want from playbook is set to non default value and the setting currently configured is non default + sent_commands.update({"enabled": want_afi.get("enabled")}) + requests.extend(self.get_delete_enabled_request(want_afi)) + if want_afi.get('verify_mac') is False and have_afi.get('verify_mac') is False: + sent_commands.update({"verify_mac": want_afi.get("verify_mac")}) + requests.extend(self.get_delete_verify_mac_request(want_afi)) + if want_afi.get('vlans') is not None and have_afi.get('vlans') is not None and have_afi.get("vlans") != []: + # gathering list of vlans to be deleted. this section also handles cases where empty list of vlans is passed in + # which means delete all vlans + to_delete_vlans = have_afi["vlans"] + if len(want_afi["vlans"]) > 0: + to_delete_vlans = list(set(have_afi.get("vlans", [])).intersection(set(want_afi.get("vlans", [])))) + to_delete = {"afi": want_afi["afi"], "vlans": to_delete_vlans} + if len(to_delete["vlans"]): + sent_commands.update({"vlans": deepcopy(to_delete_vlans)}) + requests.extend(self.get_delete_vlans_requests(to_delete)) + if want_afi.get('trusted') is not None and have_afi.get('trusted') is not None and have_afi.get('trusted') != []: + # gathering list of interfaces to be deleted. this section also handles cases where empty list of interfaces is passed in which + # means delete all trusted interfaces + to_delete_trusted = have_afi["trusted"] + if len(want_afi["trusted"]) > 0: + to_delete_trusted = want_afi["trusted"] + # removing interfaces that don't exist on device + for intf in list(to_delete_trusted): + if intf not in have_afi["trusted"]: + to_delete_trusted.remove(intf) + to_delete = {"afi": want_afi["afi"], "trusted": to_delete_trusted} + if len(to_delete["trusted"]): + sent_commands.update({"trusted": deepcopy(to_delete_trusted)}) + requests.extend(self.get_delete_trusted_requests(to_delete)) + if want_afi.get('source_bindings') is not None and have_afi.get('source_bindings') is not None and have_afi.get('source_bindings') != []: + # gathering list of source bindings to be deleted. this section also handles cases where empty list of bindings is passed in which + # means delete all trusted bindings + to_delete_bindings = have_afi["source_bindings"] + if len(want_afi["source_bindings"]) > 0: + to_delete_bindings = want_afi["source_bindings"] + # removing bindings that don't exist on device + existing_keys = [binding["mac_addr"] for binding in have_afi["source_bindings"]] + for binding in list(to_delete_bindings): + if binding["mac_addr"] not in existing_keys: + # need to check by the key since can have two different versions of same binding + to_delete_bindings.remove(binding) + to_delete = {"afi": want_afi["afi"], "source_bindings": to_delete_bindings} + if len(to_delete["source_bindings"]): + sent_commands.update({"source_bindings": deepcopy(to_delete_bindings)}) + requests.extend(self.get_delete_specific_source_bindings_requests(to_delete)) + + return sent_commands, requests + + def get_delete_enabled_request(self, afi): + '''makes and returns request to "delete" aka reset to default the enabled setting for one afi family. returns as a list''' + payload = {'openconfig-dhcp-snooping:dhcpv{v}-admin-enable'.format(v=self.afi_to_vnum(afi)): False} + return [{'path': self.enable_uri.format(v=self.afi_to_vnum(afi)), 'method': self.patch_method_value, 'data': payload}] + + def get_delete_verify_mac_request(self, afi): + '''makes and returns request to "delete" aka reset to default the config for one afi family's verify mac setting''' + payload = {'openconfig-dhcp-snooping:dhcpv{v}-verify-mac-address'.format(v=self.afi_to_vnum(afi)): True} + return [{'path': self.verify_mac_uri.format(v=self.afi_to_vnum(afi)), 'method': self.patch_method_value, 'data': payload}] + + def get_delete_vlans_requests(self, afi): + '''makes and returns request to delete the given vlans for the given afi faimily. + input expected as a dictionary of form {"afi": <ip_version>, "vlans": <list_of_vlans>}''' + requests = [] + if afi.get('vlans'): + for vlan_id in afi.get('vlans'): + requests.append({ + 'path': self.vlans_uri.format(vlan_name='Vlan' + vlan_id, v=self.afi_to_vnum(afi)), + 'method': self.delete_method_value + }) + return requests + + def get_delete_trusted_requests(self, afi): + '''makes and returns request to delete the given trusted interfaces for the given afi faimily. + input expected as a dictionary of form {"afi": <ip_version>, "trusted": [{"intf_name": <name>}...]}''' + requests = [] + if afi.get('trusted'): + for intf in afi.get('trusted'): + intf_name = intf.get('intf_name') + if intf_name: + requests.append({ + 'path': self.trusted_uri.format(name=intf_name, v=self.afi_to_vnum(afi)), + 'method': self.delete_method_value + }) + return requests + + def get_delete_all_source_bindings_request(self): + '''creates request to delete the source bindings list, which clears all bindings from both families''' + return [{'path': self.binding_uri, 'method': self.delete_method_value}] + + def get_delete_specific_source_bindings_requests(self, afi): + '''creates and builds a list of requests to delete the source bindings listed in the given afi family + input expected as a dictionary of form to_delete = {"afi": <ip_version>, "source_bindings": <list of source_bindings>}''' + requests = [] + for entry in afi.get('source_bindings'): + if entry.get('mac_addr'): + requests.append({ + 'path': self.binding_uri + '={mac},{ipv}'.format(mac=entry.get('mac_addr'), ipv=afi.get('afi')), + 'method': self.delete_method_value + }) + return requests + + def get_delete_individual_source_bindings_requests(self, afi, entry): + '''create a request to delete the given source binding entry and address family specified + by afi''' + return [{'path': self.binding_uri + '={mac},{ipv}'.format(mac=entry.get('mac_addr'), ipv=afi.get('afi')), 'method': self.delete_method_value}] + + def get_delete_replaced_groupings(self, afis): + '''builds list of requests to handle replaced state for both address families''' + modified_afi_commands = [] + requests = [] + + want_ipv4 = afis.get('want_ipv4') + have_ipv4 = afis.get('have_ipv4') + want_ipv6 = afis.get('want_ipv6') + have_ipv6 = afis.get('have_ipv6') + + if want_ipv4 and have_ipv4: + ipv4_commands, ipv4_requests = self.get_delete_replaced_groupings_afi(want_ipv4, have_ipv4) + requests.extend(ipv4_requests) + if ipv4_commands: + ipv4_commands["afi"] = want_ipv4["afi"] + modified_afi_commands.append(ipv4_commands) + if want_ipv6 and have_ipv6: + ipv6_commands, ipv6_requests = self.get_delete_replaced_groupings_afi(want_ipv6, have_ipv6) + requests.extend(ipv6_requests) + if ipv6_commands: + ipv6_commands["afi"] = want_ipv6["afi"] + modified_afi_commands.append(ipv6_commands) + + sent_commands = [] + if modified_afi_commands: + sent_commands = {"afis": modified_afi_commands} + + return sent_commands, requests + + def get_delete_replaced_groupings_afi(self, want_afi, have_afi): + '''creates and builds a list of requests to handle all parts that need to be deleted + while handling the replaced state for an address family''' + sent_commands = {} + requests = [] + diff_requested = get_diff(have_afi, want_afi, self.test_keys) + + if diff_requested.get("vlans") and "vlans" in want_afi: + # delete any vlans that are different + to_delete = {"afi": have_afi["afi"], "vlans": diff_requested["vlans"]} + sent_commands["vlans"] = deepcopy(diff_requested["vlans"]) + requests.extend(self.get_delete_vlans_requests(to_delete)) + if diff_requested.get('trusted') and 'trusted' in want_afi: + # delete anything that has a difference, covers things that are + # in have but not want and things in both but modified + to_delete = {"afi": have_afi["afi"], "trusted": diff_requested["trusted"]} + sent_commands["trusted"] = deepcopy(diff_requested["trusted"]) + requests.extend(self.get_delete_trusted_requests(to_delete)) + if diff_requested.get('source_bindings') and 'source_bindings' in want_afi: + # assuming source bindings considered a replaceable subsection ie the list afterwards + # should look exactly like what was passed into want + if want_afi["source_bindings"] == []: + # replaced told want to replace existing with blank list, only thing to do is delete existing bindings for family + sent_commands["source_bindings"] = deepcopy(have_afi["source_bindings"]) + requests.extend(self.get_delete_specific_source_bindings_requests(have_afi)) + else: + sent_commands["source_bindings"] = deepcopy(diff_requested["source_bindings"]) + for entry in diff_requested["source_bindings"]: + requests.extend(self.get_delete_individual_source_bindings_requests(have_afi, entry)) + return sent_commands, requests + + def prep_replaced_to_merge(self, diff, afis): + '''preps results from a get diff for use in merging. needed for source bindings to have all data needed. get diff only returns the fields that + are different in each source binding when all data for it is needed instead. Fills in each source binding in diff with what is found for it in afis''' + if not diff or not diff.get("afis"): + return {} + for diff_afi in diff["afis"]: + if "source_bindings" in diff_afi: + for binding in diff_afi["source_bindings"]: + binding.update(self.match_binding(binding["mac_addr"], afis["want_" + diff_afi["afi"]]["source_bindings"])) + + @staticmethod + def match_binding(mac_addr, bindings): + for binding in bindings: + if binding["mac_addr"] == mac_addr: + return binding + return {} + + @staticmethod + def afi_to_vnum(afi): + if afi.get('afi') == 'ipv6': + return '6' + else: + return '4' 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 index acf985ebf..33607817b 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# © Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# © Copyright 2023 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) """ @@ -18,6 +18,15 @@ try: except ImportError: from urllib.parse import quote +""" +The use of natsort causes sanity error due to it is not available in python version currently used. +When natsort becomes available, the code here and below using it will be applied. +from natsort import ( + natsorted, + ns +) +""" +from copy import deepcopy from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -33,14 +42,22 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s ) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.interfaces_util import ( build_interfaces_create_request, + retrieve_default_intf_speed, + retrieve_port_group_interfaces ) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( get_diff, update_states, - normalize_interface_name + normalize_interface_name, + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + get_new_config, + get_formatted_config_diff ) from ansible.module_utils._text import to_native from ansible.module_utils.connection import ConnectionError +import re import traceback LIB_IMP_ERR = None @@ -53,8 +70,54 @@ except Exception as e: ERR_MSG = to_native(e) LIB_IMP_ERR = traceback.format_exc() +GET = 'get' PATCH = 'patch' DELETE = 'delete' +url = 'data/openconfig-interfaces:interfaces/interface=%s' +eth_conf_url = "/openconfig-if-ethernet:ethernet/config" + +port_num_regex = re.compile(r'[\d]{1,4}$') +non_eth_attribute = ('description', 'mtu', 'enabled') +eth_attribute = ('description', 'mtu', 'enabled', 'auto_negotiate', 'speed', 'fec', 'advertised_speed') + +attributes_default_value = { + "description": '', + "mtu": 9100, + "enabled": False, + "auto_negotiate": False, + "fec": 'FEC_DISABLED', + "advertised_speed": [] +} +default_intf_speeds = {} +port_group_interfaces = None + + +def __derive_interface_config_delete_op(key_set, command, exist_conf): + new_conf = exist_conf + intf_name = command['name'] + + for attr in eth_attribute: + if attr in command: + if attr == "speed": + new_conf[attr] = default_intf_speeds[intf_name] + elif attr == "advertised_speed": + if new_conf[attr] is not None: + new_conf[attr] = list(set(new_conf[attr]).difference(command[attr])) + if new_conf[attr] == []: + new_conf[attr] = None + elif attr == "auto_negotiate": + new_conf[attr] = False + if new_conf.get('advertised_speed') is not None: + new_conf['advertised_speed'] = None + else: + new_conf[attr] = attributes_default_value[attr] + + return True, new_conf + + +TEST_KEYS_formatted_diff = [ + {'config': {'name': '', '__delete_op': __derive_interface_config_delete_op}}, +] class Interfaces(ConfigBase): @@ -71,9 +134,6 @@ class Interfaces(ConfigBase): 'interfaces', ] - params = ('description', 'mtu', 'enabled') - delete_flag = False - def __init__(self, module): super(Interfaces, self).__init__(module) @@ -100,7 +160,7 @@ class Interfaces(ConfigBase): warnings = list() existing_interfaces_facts = self.get_interfaces_facts() - commands, requests = self.set_config(existing_interfaces_facts) + commands, requests = self.set_config(existing_interfaces_facts, warnings) if commands and len(requests) > 0: if not self._module.check_mode: try: @@ -116,10 +176,27 @@ class Interfaces(ConfigBase): if result['changed']: result['after'] = changed_interfaces_facts + new_config = changed_interfaces_facts + old_config = existing_interfaces_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_interfaces_facts, + TEST_KEYS_formatted_diff) + # See the above comment about natsort module + # new_config = natsorted(new_config, key=lambda x: x['name']) + # For time-being, use simple "sort" + new_config.sort(key=lambda x: x['name']) + result['after(generated)'] = new_config + old_config.sort(key=lambda x: x['name']) + + if self._module._diff: + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) result['warnings'] = warnings return result - def set_config(self, existing_interfaces_facts): + def set_config(self, existing_interfaces_facts, warnings): """ Collect the configuration from the args passed to the module, collect the current configuration (as a dict from facts) @@ -128,12 +205,51 @@ class Interfaces(ConfigBase): to the desired configuration """ want = self._module.params['config'] - normalize_interface_name(want, self._module) have = existing_interfaces_facts + self.filter_out_mgmt_interface(want, have) - resp = self.set_state(want, have) + new_want, new_have = self.validate_config(want, have, warnings) + resp = self.set_state(new_want, new_have) return to_list(resp) + def validate_config(self, want, have, warnings): + new_want = deepcopy(want) + new_have = deepcopy(have) + normalize_interface_name(new_want, self._module) + for cmd in new_have: + # If auto_neg is true, ignore speed + if cmd.get('auto_negotiate') is True: + if cmd.get('speed'): + cmd.pop('speed') + elif cmd.get('advertised_speed'): + cmd.pop('advertised_speed') + + if new_want: + for cmd in new_want: + intf = next((cfg for cfg in new_have if cfg['name'] == cmd['name']), None) + state = self._module.params['state'] + if cmd.get('advertised_speed'): + cmd['advertised_speed'].sort() + + if state != "deleted": + if intf: + want_autoneg = cmd.get('auto_negotiate') + have_autoneg = intf.get('auto_negotiate') + want_speed = cmd.get('speed') + want_ads = cmd.get('advertised_speed') + + if want_speed is not None: + if want_autoneg or (want_ads and have_autoneg): + warnings.append("Speed cannot be configured when autoneg is enabled") + cmd.pop('speed') + + if want_ads is not None: + if want_autoneg is False or (not want_autoneg and not have_autoneg): + warnings.append("Advertised speed cannot be configured when autoneg is disabled") + cmd.pop('advertised_speed') + + return new_want, new_have + def set_state(self, want, have): """ Select the appropriate function based on the state provided @@ -149,18 +265,17 @@ class Interfaces(ConfigBase): # 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) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': commands, requests = self._state_merged(want, have, diff) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) return commands, requests - def _state_replaced(self, want, have, diff): + def _state_replaced(self, want, have): """ The command generator when state is replaced :param want: the desired configuration as a dictionary @@ -170,17 +285,13 @@ class Interfaces(ConfigBase): :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 = [] + commands = [] + requests = [] + commands, requests = self.get_replaced_overridden_config(want, have, "replaced") return commands, requests - def _state_overridden(self, want, have, diff): + def _state_overridden(self, want, have): """ The command generator when state is overridden :param want: the desired configuration as a dictionary @@ -190,18 +301,9 @@ class Interfaces(ConfigBase): 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) + requests = [] + + commands, requests = self.get_replaced_overridden_config(want, have, "overridden") return commands, requests @@ -214,8 +316,8 @@ class Interfaces(ConfigBase): :returns: the commands necessary to merge the provided into the current configuration """ - commands = diff - requests = self.get_modify_interface_requests(commands, have) + commands = self.filter_commands_to_change(diff, have) + requests = self.get_interface_requests(commands, have) if commands and len(requests) > 0: commands = update_states(commands, "merged") else: @@ -223,7 +325,7 @@ class Interfaces(ConfigBase): return commands, requests - def _state_deleted(self, want, have, diff): + def _state_deleted(self, want, have): """ The command generator when state is deleted :param want: the objects from which the configuration should be removed @@ -234,34 +336,23 @@ class Interfaces(ConfigBase): of the provided objects """ # if want is none, then delete all the interfaces + + want = remove_empties_from_list(want) + delete_all = False if not want: commands = have + delete_all = True 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_del, requests = self.handle_delete_interface_config(commands, have, delete_all) commands = [] + if commands_del: + commands.extend(update_states(commands_del, "deleted")) - 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 + return commands, requests - def filter_comands_to_change(self, configs, have): + def filter_commands_to_change(self, configs, have): commands = [] if configs: for conf in configs: @@ -269,17 +360,18 @@ class Interfaces(ConfigBase): 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) + def is_this_change_required(self, conf, have): + 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 different from existing + for param in eth_attribute: + if conf.get(param) is not None and conf.get(param) != intf.get(param): + return True + else: + # if given interface is not present + return True - return self.get_interface_requests(commands, have) + return False def get_interface_requests(self, configs, have): requests = [] @@ -288,67 +380,285 @@ class Interfaces(ConfigBase): # 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} + name = conf['name'] + have_conf = next((cfg for cfg in have if cfg['name'] == name), None) + + # Create Loopback incase if not available in have + if name.startswith('Loopback'): + if not have_conf: + loopback_create_request = build_interfaces_create_request(name) + requests.append(loopback_create_request) 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) - + attribute = eth_attribute if name.startswith('Eth') else non_eth_attribute + + for attr in attribute: + if attr in conf: + c_attr = conf.get(attr) + h_attr = have_conf.get(attr) + attr_request = self.build_create_request(c_attr, h_attr, name, attr) + if attr_request: + requests.append(attr_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 build_create_request(self, c_attr, h_attr, intf_name, attr): + attributes_payload = { + "speed": 'port-speed', + "auto_negotiate": 'auto-negotiate', + "fec": 'openconfig-if-ethernet-ext2:port-fec', + "advertised_speed": 'openconfig-if-ethernet-ext2:advertised-speed' + } + + config_url = (url + eth_conf_url) % quote(intf_name, safe='') + payload = {'openconfig-if-ethernet:config': {}} + payload_attr = attributes_payload.get(attr, attr) + method = PATCH + + if attr in ('description', 'mtu', 'enabled'): + config_url = (url + '/config') % quote(intf_name, safe='') + payload = {'openconfig-interfaces:config': {}} + payload['openconfig-interfaces:config'][payload_attr] = c_attr + return {"path": config_url, "method": method, "data": payload} + + elif attr in ('fec'): + payload['openconfig-if-ethernet:config'][payload_attr] = 'openconfig-platform-types:' + c_attr + return {"path": config_url, "method": method, "data": payload} + else: + payload['openconfig-if-ethernet:config'][payload_attr] = c_attr + if attr == 'speed': + if self.is_port_in_port_group(intf_name): + self._module.fail_json(msg='Unable to configure speed in port group member. Please use port group module to change the speed') + payload['openconfig-if-ethernet:config'][payload_attr] = 'openconfig-if-ethernet:' + c_attr + if attr == 'advertised_speed': + c_ads = c_attr if c_attr else [] + h_ads = h_attr if h_attr else [] + new_ads = list(set(h_ads).union(c_ads)) + if new_ads: + payload['openconfig-if-ethernet:config'][payload_attr] = ','.join(new_ads) + + return {"path": config_url, "method": method, "data": payload} + + return [] + + def handle_delete_interface_config(self, commands, have, delete_all=False): + if not commands: + return [], [] + + commands_del, requests = [], [] + # Create URL and payload + for conf in commands: + name = conf['name'] + have_conf = next((cfg for cfg in have if cfg['name'] == name), None) + if have_conf: + lp_key_set = set(conf.keys()) + if name.startswith('Loopback'): + if delete_all or len(lp_key_set) == 1: + method = DELETE + lpbk_url = url % quote(name, safe='') + request = {"path": lpbk_url, "method": DELETE} + requests.append(request) + + commands_del.append({'name': name}) + continue + + cmd = deepcopy(have_conf) if len(lp_key_set) == 1 else deepcopy(conf) + + del_cmd = {'name': name} + attribute = eth_attribute if name.startswith('Eth') else non_eth_attribute + + for attr in attribute: + if attr in conf: + c_attr = conf.get(attr) + h_attr = have_conf.get(attr) + default_val = self.get_default_value(attr, h_attr, name) + if c_attr is not None and h_attr is not None and h_attr != default_val: + if attr == 'advertised_speed': + c_ads = c_attr if c_attr else [] + h_ads = h_attr if h_attr else [] + new_ads = list(set(h_attr).intersection(c_attr)) + if new_ads: + del_cmd.update({attr: new_ads}) + requests.append(self.build_delete_request(c_ads, h_ads, name, attr)) + else: + del_cmd.update({attr: h_attr}) + requests.append(self.build_delete_request(c_attr, h_attr, name, attr)) + if requests: + commands_del.append(del_cmd) + + return commands_del, requests + + def get_replaced_overridden_config(self, want, have, cur_state): + commands, requests = [], [] + + commands_add, commands_del = [], [] + requests_add, requests_del = [], [] + + delete_all = False + for conf in want: + name = conf['name'] + intf = next((e_intf for e_intf in have if name == e_intf['name']), None) + if name.startswith('Loopback'): + if not intf: + commands_add.append({'name': name}) + continue + + temp_conf = {} + add_conf, del_conf = {}, {} + + temp_conf['name'] = name + attribute = eth_attribute if name.startswith('Eth') else non_eth_attribute + + if not intf: + commands_add.append(conf) + else: + is_change = False + non_ads_attr_specified = False + if cur_state == "replaced": + for attr in conf: + if attr != 'name' and attr != 'advertised_speed' and conf.get(attr) is not None: + non_ads_attr_specified = True + break + else: + non_ads_attr_specified = True + + for attr in attribute: + c_attr = conf.get(attr) + h_attr = intf.get(attr) + default_val = self.get_default_value(attr, h_attr, name) + if attr != 'advertised_speed': + if c_attr is None and h_attr is not None and h_attr != default_val and non_ads_attr_specified: + del_conf[attr] = h_attr + requests_del.append(self.build_delete_request(c_attr, h_attr, name, attr)) + if c_attr is not None and c_attr != h_attr: + add_conf[attr] = c_attr + requests_add.append(self.build_create_request(c_attr, h_attr, name, attr)) + else: + c_ads = c_attr if c_attr else [] + h_ads = h_attr if h_attr else [] + new_ads = list(set(c_ads).difference(h_ads)) + delete_ads = list(set(h_ads).difference(c_ads)) + if new_ads: + add_conf[attr] = new_ads + requests_add.append(self.build_create_request(new_ads, h_attr, name, attr)) + if delete_ads: + del_conf[attr] = delete_ads + requests_del.append(self.build_delete_request(delete_ads, h_attr, name, attr)) + + if add_conf: + add_conf['name'] = name + commands_add.append(add_conf) + + if del_conf: + del_conf['name'] = name + commands_del.append(del_conf) + + if cur_state == "overridden": + for have_conf in have: + in_want = next((conf for conf in want if conf['name'] == have_conf['name']), None) + if not in_want: + del_conf = {} + for attr in attribute: + h_attr = have_conf.get(attr) + if h_attr is not None and h_attr != self.get_default_value(attr, h_attr, have_conf['name']): + del_conf[attr] = h_attr + requests_del.append(self.build_delete_request([], h_attr, have_conf['name'], attr)) + if del_conf: + del_conf['name'] = have_conf['name'] + commands_del.append(del_conf) + + if len(requests_del) > 0: + commands.extend(update_states(commands_del, "deleted")) + requests.extend(requests_del) + + if len(requests_add) > 0: + commands.extend(update_states(commands_add, cur_state)) + requests.extend(requests_add) - 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 + return commands, requests + + def build_delete_request(self, c_attr, h_attr, intf_name, attr): + method = DELETE + attributes_payload = { + "speed": 'port-speed', + "auto_negotiate": 'auto-negotiate', + "fec": 'openconfig-if-ethernet-ext2:port-fec', + "advertised_speed": 'openconfig-if-ethernet-ext2:advertised-speed' + } + + config_url = (url + eth_conf_url) % quote(intf_name, safe='') + payload = {'openconfig-if-ethernet:config': {}} + payload_attr = attributes_payload.get(attr, attr) + + if attr in ('description', 'mtu', 'enabled'): + attr_url = "/config/" + payload_attr + config_url = (url + attr_url) % quote(intf_name, safe='') + return {"path": config_url, "method": method} + + elif attr in ('fec'): + payload_attr = attributes_payload[attr] + payload['openconfig-if-ethernet:config'][payload_attr] = 'FEC_DISABLED' + return {"path": config_url, "method": PATCH, "data": payload} else: - ret_flag = True + payload_attr = attributes_payload[attr] + if attr == 'auto_negotiate': + # For auto-negotiate, we assign value to False since deleting the attribute will become None if deleted + # In case, if auto-negotiate is disabled, both speed and advertised_speed will have default value. + payload['openconfig-if-ethernet:config'][payload_attr] = False + return {"path": config_url, "method": PATCH, "data": payload} + + if attr == 'speed': + attr_url = eth_conf_url + "/" + attributes_payload[attr] + del_config_url = (url + attr_url) % quote(intf_name, safe='') + return {"path": del_config_url, "method": method} + + if attr == 'advertised_speed': + new_ads = list(set(h_attr).difference(c_attr)) + if new_ads: + payload['openconfig-if-ethernet:config'][payload_attr] = ','.join(new_ads) + return {"path": config_url, "method": PATCH, "data": payload} + else: + attr_url = eth_conf_url + "/" + attributes_payload[attr] + del_config_url = (url + attr_url) % quote(intf_name, safe='') + return {"path": del_config_url, "method": method} + return {} + + # Utils + def get_default_value(self, attr, h_attr, intf_name): + if attr == 'speed': + default_val = self._retrieve_default_intf_speed(intf_name) + if default_val == 'SPEED_DEFAULT': + # Incase if the port belongs to port-group, we can not able to delete the speed + default_val = h_attr + return default_val + else: + return attributes_default_value[attr] + + def filter_out_mgmt_interface(self, want, have): + if want: + mgmt_intf = next((intf for intf in want if intf['name'] == 'Management0'), None) + if mgmt_intf: + self._module.fail_json(msg='Management interface should not be configured.') + + for intf in have: + if intf['name'] == 'Management0': + have.remove(intf) + break + + def is_port_in_port_group(self, intf_name): + global port_group_interfaces + if port_group_interfaces is None: + port_group_interfaces = retrieve_port_group_interfaces(self._module) + port_num = re.search(port_num_regex, intf_name) + port_num = int(port_num.group(0)) + if port_num in port_group_interfaces: + return True - return ret_flag + return False - def build_create_payload(self, conf): - temp_conf = dict() - temp_conf['name'] = conf['name'] + def _retrieve_default_intf_speed(self, intf_name): + # To avoid multiple get requests + if self.is_port_in_port_group(intf_name): + return "SPEED_DEFAULT" - 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 + if default_intf_speeds.get(intf_name) is None: + default_intf_speeds[intf_name] = retrieve_default_intf_speed(self._module, intf_name) + return default_intf_speeds[intf_name] diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/ip_neighbor/ip_neighbor.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/ip_neighbor/ip_neighbor.py new file mode 100644 index 000000000..ab3a4dde6 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/ip_neighbor/ip_neighbor.py @@ -0,0 +1,420 @@ +# +# -*- 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_ip_neighbor 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, + 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 ( + get_diff, + update_states, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + get_new_config, + get_formatted_config_diff +) +from ansible.module_utils.connection import ConnectionError + +GET = 'get' +PATCH = 'patch' +PUT = 'put' +DELETE = 'delete' +GLB_URL = 'data/openconfig-neighbor:neighbor-globals/neighbor-global' +URL = 'data/openconfig-neighbor:neighbor-globals/neighbor-global=Values' +CONFIG_URL = 'data/openconfig-neighbor:neighbor-globals/neighbor-global=Values/config' + +IP_NEIGH_CONFIG_DEFAULT = { + 'ipv4_arp_timeout': 180, + 'ipv4_drop_neighbor_aging_time': 300, + 'ipv6_drop_neighbor_aging_time': 300, + 'ipv6_nd_cache_expiry': 180, + 'num_local_neigh': 0 +} + +IP_NEIGH_CONFIG_REQ_DEFAULT = { + 'name': 'Values', + 'ipv4-arp-timeout': 180, + 'ipv4-drop-neighbor-aging-time': 300, + 'ipv6-drop-neighbor-aging-time': 300, + 'ipv6-nd-cache-expiry': 180, + 'num-local-neigh': 0 +} + + +def __derive_ip_neighbor_config_delete_op(key_set, command, exist_conf): + new_conf = exist_conf + + if 'ipv4_arp_timeout' in command: + new_conf['ipv4_arp_timeout'] = IP_NEIGH_CONFIG_DEFAULT['ipv4_arp_timeout'] + + if 'ipv4_drop_neighbor_aging_time' in command: + new_conf['ipv4_drop_neighbor_aging_time'] = \ + IP_NEIGH_CONFIG_DEFAULT['ipv4_drop_neighbor_aging_time'] + + if 'ipv6_drop_neighbor_aging_time' in command: + new_conf['ipv6_drop_neighbor_aging_time'] = \ + IP_NEIGH_CONFIG_DEFAULT['ipv6_drop_neighbor_aging_time'] + + if 'ipv6_nd_cache_expiry' in command: + new_conf['ipv6_nd_cache_expiry'] = IP_NEIGH_CONFIG_DEFAULT['ipv6_nd_cache_expiry'] + + if 'num_local_neigh' in command: + new_conf['num_local_neigh'] = IP_NEIGH_CONFIG_DEFAULT['num_local_neigh'] + + return True, new_conf + + +TEST_KEYS_formatted_diff = [ + {'__default_ops': {'__delete_op': __derive_ip_neighbor_config_delete_op}}, +] + + +class Ip_neighbor(ConfigBase): + """ + The sonic_ip_neighbor class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'ip_neighbor', + ] + + def __init__(self, module): + super(Ip_neighbor, self).__init__(module) + + def get_ip_neighbor_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) + ip_neighbor_facts = facts['ansible_network_resources'].get('ip_neighbor') + if not ip_neighbor_facts: + requests = self.build_create_all_requests() + try: + edit_config(self._module, to_request(self._module, requests)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources) + ip_neighbor_facts = facts['ansible_network_resources'].get('ip_neighbor') + + if not ip_neighbor_facts: + err_msg = "IP neighbor module: get facts failed." + self._module.fail_json(msg=err_msg, code=500) + + return ip_neighbor_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_ip_neighbor_facts = self.get_ip_neighbor_facts() + + commands, requests = self.set_config(existing_ip_neighbor_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_ip_neighbor_facts = self.get_ip_neighbor_facts() + + result['before'] = existing_ip_neighbor_facts + if result['changed']: + result['after'] = changed_ip_neighbor_facts + + new_config = changed_ip_neighbor_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_ip_neighbor_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_ip_neighbor_facts, + new_config, + self._module._verbosity) + result['warnings'] = warnings + return result + + def set_config(self, existing_ip_neighbor_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_ip_neighbor_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'] + want = remove_empties(want) + + if state == 'merged': + commands, requests = self._state_merged(want, have) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) + + return commands, requests + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = get_diff(want, have) + requests = [] + + if commands: + requests = self.build_merge_requests(commands) + + 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 + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + delete_all = False + if not want: + tmp_commands = have + delete_all = True + else: + tmp_commands = want + tmp_commands = self.preprocess_delete_commands(tmp_commands, have) + + commands = get_diff(tmp_commands, IP_NEIGH_CONFIG_DEFAULT) + + requests = [] + if commands: + requests = self.build_delete_requests(commands, delete_all) + + if len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + new_want = self.augment_want_with_default(want) + commands = get_diff(new_want, have) + + requests = [] + if commands: + requests = self.build_merge_requests(commands) + + if len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + new_want = self.augment_want_with_default(want) + commands = get_diff(new_want, have) + + requests = [] + if commands: + requests = self.build_merge_requests(commands) + + if len(requests) > 0: + commands = update_states(commands, "overridden") + else: + commands = [] + + return commands, requests + + def preprocess_delete_commands(self, commands, have): + new_commands = dict() + + if 'ipv4_arp_timeout' in commands: + new_commands['ipv4_arp_timeout'] = have['ipv4_arp_timeout'] + + if 'ipv4_drop_neighbor_aging_time' in commands: + new_commands['ipv4_drop_neighbor_aging_time'] = have['ipv4_drop_neighbor_aging_time'] + + if 'ipv6_drop_neighbor_aging_time' in commands: + new_commands['ipv6_drop_neighbor_aging_time'] = have['ipv6_drop_neighbor_aging_time'] + + if 'ipv6_nd_cache_expiry' in commands: + new_commands['ipv6_nd_cache_expiry'] = have['ipv6_nd_cache_expiry'] + + if 'num_local_neigh' in commands: + new_commands['num_local_neigh'] = have['num_local_neigh'] + + return new_commands + + def augment_want_with_default(self, want): + new_want = IP_NEIGH_CONFIG_DEFAULT + + if 'ipv4_arp_timeout' in want: + new_want['ipv4_arp_timeout'] = want['ipv4_arp_timeout'] + + if 'ipv4_drop_neighbor_aging_time' in want: + new_want['ipv4_drop_neighbor_aging_time'] = want['ipv4_drop_neighbor_aging_time'] + + if 'ipv6_drop_neighbor_aging_time' in want: + new_want['ipv6_drop_neighbor_aging_time'] = want['ipv6_drop_neighbor_aging_time'] + + if 'ipv6_nd_cache_expiry' in want: + new_want['ipv6_nd_cache_expiry'] = want['ipv6_nd_cache_expiry'] + + if 'num_local_neigh' in want: + new_want['num_local_neigh'] = want['num_local_neigh'] + + return new_want + + def build_create_all_requests(self): + requests = [] + payload = { + "openconfig-neighbor:neighbor-global": + [{"name": "Values", + "config": IP_NEIGH_CONFIG_REQ_DEFAULT}] + } + method = PUT + + request = {"path": GLB_URL, "method": method, "data": payload} + requests.append(request) + return requests + + def build_merge_requests(self, conf): + requests = [] + ip_neigh_config = dict() + + if 'ipv4_arp_timeout' in conf: + ip_neigh_config['ipv4-arp-timeout'] = conf['ipv4_arp_timeout'] + + if 'ipv4_drop_neighbor_aging_time' in conf: + ip_neigh_config['ipv4-drop-neighbor-aging-time'] = conf['ipv4_drop_neighbor_aging_time'] + + if 'ipv6_drop_neighbor_aging_time' in conf: + ip_neigh_config['ipv6-drop-neighbor-aging-time'] = conf['ipv6_drop_neighbor_aging_time'] + + if 'ipv6_nd_cache_expiry' in conf: + ip_neigh_config['ipv6-nd-cache-expiry'] = conf['ipv6_nd_cache_expiry'] + + if 'num_local_neigh' in conf: + ip_neigh_config['num-local-neigh'] = conf['num_local_neigh'] + + if ip_neigh_config: + payload = {'config': ip_neigh_config} + method = PATCH + requests = {"path": CONFIG_URL, "method": method, "data": payload} + + return requests + + def build_delete_requests(self, conf, delete_all): + requests = [] + method = DELETE + + if delete_all: + request = {"path": URL, "method": method} + requests.append(request) + return requests + + if 'ipv4_arp_timeout' in conf: + req_url = CONFIG_URL + '/ipv4-arp-timeout' + request = {"path": req_url, "method": method} + requests.append(request) + + if 'ipv4_drop_neighbor_aging_time' in conf: + req_url = CONFIG_URL + '/ipv4-drop-neighbor-aging-time' + request = {"path": req_url, "method": method} + requests.append(request) + + if 'ipv6_drop_neighbor_aging_time' in conf: + req_url = CONFIG_URL + '/ipv6-drop-neighbor-aging-time' + request = {"path": req_url, "method": method} + requests.append(request) + + if 'ipv6_nd_cache_expiry' in conf: + req_url = CONFIG_URL + '/ipv6-nd-cache-expiry' + request = {"path": req_url, "method": method} + requests.append(request) + + if 'num_local_neigh' in conf: + req_url = CONFIG_URL + '/num-local-neigh' + request = {"path": req_url, "method": method} + requests.append(request) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_acls/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_acls/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_acls/l2_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_acls/l2_acls.py new file mode 100644 index 000000000..392e69039 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l2_acls/l2_acls.py @@ -0,0 +1,602 @@ +# +# -*- 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_l2_acls 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 ast import literal_eval + +from ansible.module_utils._text import to_text +from ansible.module_utils.common.validation import check_required_arguments +from ansible.module_utils.connection import ConnectionError +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, + remove_empties, + validate_config +) +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 +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) + +DELETE = 'delete' +PATCH = 'patch' +POST = 'post' + +TEST_KEYS_formatted_diff = [ + {'config': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'rules': {'sequence_num': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] + +L2_ACL_TYPE = 'ACL_L2' +ETHERTYPE_FORMAT = '0x{:04x}' + +ethertype_value_to_protocol_map = { + '0x0800': 'ipv4', + '0x0806': 'arp', + '0x86dd': 'ipv6' +} +pcp_value_to_traffic_map = { + 0: 'be', + 1: 'bk', + 2: 'ee', + 3: 'ca', + 4: 'vi', + 5: 'vo', + 6: 'ic', + 7: 'nc' +} + +# Spec value to payload value mappings +action_value_to_payload_map = { + 'permit': 'ACCEPT', + 'discard': 'DISCARD', + 'do-not-nat': 'DO_NOT_NAT', + 'deny': 'DROP', + 'transit': 'TRANSIT' +} +ethertype_protocol_to_payload_map = { + 'arp': 'ETHERTYPE_ARP', + 'ipv4': 'ETHERTYPE_IPV4', + 'ipv6': 'ETHERTYPE_IPV6' +} +ethertype_value_to_payload_map = { + '0x8847': 'ETHERTYPE_MPLS', + '0x88cc': 'ETHERTYPE_LLDP', + '0x8915': 'ETHERTYPE_ROCE' +} +pcp_traffic_to_value_map = {v: k for k, v in pcp_value_to_traffic_map.items()} + + +class L2_acls(ConfigBase): + """ + The sonic_l2_acls class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'l2_acls', + ] + + acl_path = 'data/openconfig-acl:acl/acl-sets/acl-set' + l2_acl_path = 'data/openconfig-acl:acl/acl-sets/acl-set={acl_name},ACL_L2' + l2_acl_rule_path = 'data/openconfig-acl:acl/acl-sets/acl-set={acl_name},ACL_L2/acl-entries' + l2_acl_remark_path = 'data/openconfig-acl:acl/acl-sets/acl-set={acl_name},ACL_L2/config/description' + + def __init__(self, module): + super(L2_acls, self).__init__(module) + + def get_l2_acls_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_acls_facts = facts['ansible_network_resources'].get('l2_acls') + if not l2_acls_facts: + return [] + return l2_acls_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + + existing_l2_acls_facts = self.get_l2_acls_facts() + commands, requests = self.set_config(existing_l2_acls_facts) + if commands: + if not self._module.check_mode: + try: + edit_config(self._module, to_request(self._module, requests)) + except ConnectionError as exc: + self._handle_failure_response(exc) + + result['changed'] = True + + changed_l2_acls_facts = self.get_l2_acls_facts() + + result['before'] = existing_l2_acls_facts + if result['changed']: + result['after'] = changed_l2_acls_facts + + result['commands'] = commands + + new_config = changed_l2_acls_facts + old_config = existing_l2_acls_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_l2_acls_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + if self._module._diff: + self.sort_config(new_config) + self.sort_config(old_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) + result['warnings'] = warnings + return result + + def set_config(self, existing_l2_acls_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: + want = self.validate_and_normalize_config(want) + else: + want = [] + + have = existing_l2_acls_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 in ('merged', 'overridden', 'replaced'): + commands, requests = self._state_merged_overridden_replaced(want, have, state) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + + return commands, requests + + def _handle_failure_response(self, connection_error): + log = None + try: + response = literal_eval(connection_error.args[0]) + error_app_tag = response['ietf-restconf:errors']['error'][0].get('error-app-tag') + except Exception: + pass + else: + if error_app_tag == 'too-many-elements': + log = 'Exceeds maximum number of ACL / ACL Rules' + elif error_app_tag == 'update-not-allowed': + log = 'Creating ACLs with same name and different type not allowed' + + if log: + response.update({u'log': log}) + self._module.fail_json(msg=to_text(response), code=connection_error.code) + else: + self._module.fail_json(msg=str(connection_error), code=connection_error.code) + + def _state_merged_overridden_replaced(self, want, have, state): + """ The command generator when state is merged/overridden/replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + add_commands = [] + del_commands = [] + commands = [] + + add_requests = [] + del_requests = [] + requests = [] + + have_dict = self._convert_config_list_to_dict(have) + want_dict = self._convert_config_list_to_dict(want) + have_acl_names = set(have_dict.keys()) + want_acl_names = set(want_dict.keys()) + + if state == 'overridden': + # Delete non-modified ACLs + for acl_name in have_acl_names.difference(want_acl_names): + del_commands.append({'name': acl_name}) + del_requests.append(self.get_delete_l2_acl_request(acl_name)) + + # Modify existing ACLs + for acl_name in want_acl_names.intersection(have_acl_names): + acl_add_command = {'name': acl_name} + acl_del_command = {'name': acl_name} + rule_add_commands = [] + rule_del_commands = [] + + have_acl = have_dict[acl_name] + want_acl = want_dict[acl_name] + if not want_acl['remark']: + if have_acl['remark'] and state in ('replaced', 'overridden'): + acl_del_command['remark'] = have_acl['remark'] + del_requests.append(self.get_delete_l2_acl_remark_request(acl_name)) + else: + if want_acl['remark'] != have_acl['remark']: + acl_add_command['remark'] = want_acl['remark'] + add_requests.append(self.get_create_l2_acl_remark_request(acl_name, want_acl['remark'])) + + have_seq_nums = set(have_acl['rules'].keys()) + want_seq_nums = set(want_acl['rules'].keys()) + + if state in ('replaced', 'overridden'): + # Delete non-modified rules + for seq_num in have_seq_nums.difference(want_seq_nums): + rule_del_commands.append({'sequence_num': seq_num}) + del_requests.append(self.get_delete_l2_acl_rule_request(acl_name, seq_num)) + + for seq_num in want_seq_nums.intersection(have_seq_nums): + # Replace existing rules + if have_acl['rules'][seq_num] != want_acl['rules'][seq_num]: + if state == 'merged': + self._module.fail_json( + msg="Cannot update existing sequence {0} of L2 ACL {1} with state merged." + " Please use state replaced or overridden.".format(seq_num, acl_name) + ) + + rule_del_commands.append({'sequence_num': seq_num}) + del_requests.append(self.get_delete_l2_acl_rule_request(acl_name, seq_num)) + + rule_add_commands.append(want_acl['rules'][seq_num]) + add_requests.append(self.get_create_l2_acl_rule_request(acl_name, seq_num, want_acl['rules'][seq_num])) + + # Add new rules + for seq_num in want_seq_nums.difference(have_seq_nums): + rule_add_commands.append(want_acl['rules'][seq_num]) + add_requests.append(self.get_create_l2_acl_rule_request(acl_name, seq_num, want_acl['rules'][seq_num])) + + if rule_del_commands: + acl_del_command['rules'] = rule_del_commands + if rule_add_commands: + acl_add_command['rules'] = rule_add_commands + + if acl_del_command.get('rules') or acl_del_command.get('remark'): + del_commands.append(acl_del_command) + if acl_add_command.get('rules') or acl_add_command.get('remark'): + add_commands.append(acl_add_command) + + # Add new ACLs + for acl_name in want_acl_names.difference(have_acl_names): + acl_add_command = {'name': acl_name} + add_requests.append(self.get_create_l2_acl_request(acl_name)) + + want_acl = want_dict[acl_name] + if want_acl['remark']: + acl_add_command['remark'] = want_acl['remark'] + add_requests.append(self.get_create_l2_acl_remark_request(acl_name, want_acl['remark'])) + + # Add new rules + want_seq_nums = set(want_acl['rules'].keys()) + if want_seq_nums: + acl_add_command['rules'] = [] + for seq_num in want_seq_nums: + acl_add_command['rules'].append(want_acl['rules'][seq_num]) + add_requests.append(self.get_create_l2_acl_rule_request(acl_name, seq_num, want_acl['rules'][seq_num])) + + add_commands.append(acl_add_command) + + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests + + if add_commands: + commands.extend(update_states(add_commands, state)) + requests.extend(add_requests) + + 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: + for acl in have: + commands.append({'name': acl['name']}) + requests.append(self.get_delete_l2_acl_request(acl['name'])) + else: + have_dict = self._convert_config_list_to_dict(have) + want_dict = self._convert_config_list_to_dict(want) + have_acl_names = set(have_dict.keys()) + want_acl_names = set(want_dict.keys()) + + # Delete existing ACLs + for acl_name in want_acl_names.intersection(have_acl_names): + have_acl = have_dict[acl_name] + want_acl = want_dict[acl_name] + + # Delete entire ACL if only the name is specified + if not want_acl['remark'] and not want_acl['rules']: + commands.append({'name': acl_name}) + requests.append(self.get_delete_l2_acl_request(acl_name)) + continue + + acl_del_command = {'name': acl_name} + rule_del_commands = [] + have_seq_nums = set(have_acl['rules'].keys()) + want_seq_nums = set(want_acl['rules'].keys()) + + if want_acl['remark'] and want_acl['remark'] == have_acl['remark']: + acl_del_command['remark'] = want_acl['remark'] + requests.append(self.get_delete_l2_acl_remark_request(acl_name)) + + # Delete existing rules + # When state is deleted, options other than sequence_num are not considered + for seq_num in want_seq_nums.intersection(have_seq_nums): + rule_del_commands.append({'sequence_num': seq_num}) + requests.append(self.get_delete_l2_acl_rule_request(acl_name, seq_num)) + + if rule_del_commands: + acl_del_command['rules'] = rule_del_commands + + if acl_del_command.get('rules') or acl_del_command.get('remark'): + commands.append(acl_del_command) + + commands = update_states(commands, "deleted") + return commands, requests + + def get_create_l2_acl_request(self, acl_name): + """Get request to create L2 ACL with specified name""" + url = self.acl_path + payload = { + 'acl-set': [{ + 'name': acl_name, + 'type': L2_ACL_TYPE, + 'config': { + 'name': acl_name, + 'type': L2_ACL_TYPE + } + }] + } + + return {'path': url, 'method': PATCH, 'data': payload} + + def get_create_l2_acl_remark_request(self, acl_name, remark): + """Get request to add given remark to the specified L2 ACL""" + url = self.l2_acl_remark_path.format(acl_name=acl_name) + payload = {'description': remark} + return {'path': url, 'method': PATCH, 'data': payload} + + def get_create_l2_acl_rule_request(self, acl_name, seq_num, rule): + """Get request to create a rule with given sequence number + and configuration in the specified L2 ACL + """ + url = self.l2_acl_rule_path.format(acl_name=acl_name) + payload = { + 'openconfig-acl:acl-entry': [{ + 'sequence-id': seq_num, + 'config': { + 'sequence-id': seq_num + }, + 'l2': { + 'config': {} + }, + 'actions': { + 'config': { + 'forwarding-action': action_value_to_payload_map[rule['action']] + } + } + }] + } + rule_l2_config = payload['openconfig-acl:acl-entry'][0]['l2']['config'] + + if rule['source'].get('host'): + rule_l2_config['source-mac'] = rule['source']['host'] + elif rule['source'].get('address'): + rule_l2_config['source-mac'] = rule['source']['address'] + rule_l2_config['source-mac-mask'] = rule['source']['address_mask'] + + if rule['destination'].get('host'): + rule_l2_config['destination-mac'] = rule['destination']['host'] + elif rule['destination'].get('address'): + rule_l2_config['destination-mac'] = rule['destination']['address'] + rule_l2_config['destination-mac-mask'] = rule['destination']['address_mask'] + + if rule.get('ethertype'): + if rule['ethertype'].get('value'): + rule_l2_config['ethertype'] = ethertype_value_to_payload_map.get(rule['ethertype']['value'], int(rule['ethertype']['value'], 16)) + else: + rule_l2_config['ethertype'] = ethertype_protocol_to_payload_map[next(iter(rule['ethertype']))] + + if rule.get('vlan_id') is not None: + rule_l2_config['vlanid'] = rule['vlan_id'] + + if rule.get('vlan_tag_format') and rule['vlan_tag_format'].get('multi_tagged'): + rule_l2_config['vlan-tag-format'] = 'openconfig-acl-ext:MULTI_TAGGED' + + if rule.get('dei') is not None: + rule_l2_config['dei'] = rule['dei'] + + if rule.get('pcp'): + if rule['pcp'].get('traffic_type'): + rule_l2_config['pcp'] = pcp_traffic_to_value_map[rule['pcp']['traffic_type']] + else: + rule_l2_config['pcp'] = rule['pcp']['value'] + rule_l2_config['pcp-mask'] = rule['pcp']['mask'] + + if rule.get('remark'): + payload['openconfig-acl:acl-entry'][0]['config']['description'] = rule['remark'] + + return {'path': url, 'method': POST, 'data': payload} + + def get_delete_l2_acl_request(self, acl_name): + """Get request to delete L2 ACL with specified name""" + url = self.l2_acl_path.format(acl_name=acl_name) + return {'path': url, 'method': DELETE} + + def get_delete_l2_acl_remark_request(self, acl_name): + """Get request to delete remark of the specified L2 ACL""" + url = self.l2_acl_remark_path.format(acl_name=acl_name) + return {'path': url, 'method': DELETE} + + def get_delete_l2_acl_rule_request(self, acl_name, seq_num): + """Get request to delete the rule with given sequence number + in the specified L2 ACL + """ + url = self.l2_acl_rule_path.format(acl_name=acl_name) + url += '/acl-entry={0}'.format(seq_num) + return {'path': url, 'method': DELETE} + + def validate_and_normalize_config(self, config_list): + """Validate and normalize the given config""" + # Remove empties and validate the config with argument spec + updated_config_list = [remove_empties(config) for config in config_list] + validate_config(self._module.argument_spec, {'config': updated_config_list}) + + state = self._module.params['state'] + # When state is deleted, options other than sequence_num are not considered + if state == 'deleted': + return updated_config_list + + for acl in updated_config_list: + if not acl.get('rules'): + continue + + for rule in acl['rules']: + self._check_required(['action', 'source', 'destination'], rule, ['config', 'rules']) + for endpoint in ('source', 'destination'): + if rule[endpoint].get('any') is False: + self._invalid_rule('True is the only valid value for {0} -> any'.format(endpoint), acl['name'], rule['sequence_num']) + elif rule[endpoint].get('host'): + rule[endpoint]['host'] = rule[endpoint]['host'].lower() + elif rule[endpoint].get('address'): + rule[endpoint]['address'] = rule[endpoint]['address'].lower() + rule[endpoint]['address_mask'] = rule[endpoint]['address_mask'].lower() + + self._normalize_ethertype(rule) + self._normalize_pcp(rule) + self._normalize_vlan_tag_format(rule) + + return updated_config_list + + def _invalid_rule(self, err_msg, acl_name, seq_num): + self._module.fail_json(msg='L2 ACL {0}, sequence number {1}: {2}'.format(acl_name, seq_num, err_msg)) + + def _check_required(self, required_parameters, parameters, options_context=None): + if required_parameters: + spec = {} + for parameter in required_parameters: + spec[parameter] = {'required': True} + + try: + check_required_arguments(spec, parameters, options_context) + except TypeError as exc: + self._module.fail_json(msg=str(exc)) + + @staticmethod + def _normalize_ethertype(rule): + ethertype = rule.get('ethertype') + if ethertype: + if ethertype.get('value'): + value = ethertype.pop('value') + if value.startswith('0x'): + value = ETHERTYPE_FORMAT.format(int(value, 16)) + else: + # If the hexadecimal number is not enclosed within + # quotes, it will be passed as a string after being + # converted to decimal. + value = ETHERTYPE_FORMAT.format(int(value, 10)) + + if value in ethertype_value_to_protocol_map: + ethertype[ethertype_value_to_protocol_map[value]] = True + else: + ethertype['value'] = value + else: + # Remove ethertype option if its value is False + if not next(iter(ethertype.values())): + del rule['ethertype'] + + @staticmethod + def _normalize_pcp(rule): + pcp = rule.get('pcp') + if pcp and pcp.get('value') is not None and pcp.get('mask') is None: + pcp['traffic_type'] = pcp_value_to_traffic_map[pcp['value']] + del pcp['value'] + + @staticmethod + def _normalize_vlan_tag_format(rule): + vlan_tag_format = rule.get('vlan_tag_format') + # Remove vlan_tag_format option if the value is False + if vlan_tag_format and not vlan_tag_format.get('multi_tagged'): + del rule['vlan_tag_format'] + + @staticmethod + def _convert_config_list_to_dict(config_list): + config_dict = {} + for config in config_list: + acl_name = config['name'] + config_dict[acl_name] = {} + config_dict[acl_name]['remark'] = config.get('remark') + config_dict[acl_name]['rules'] = {} + if config.get('rules'): + for rule in config['rules']: + config_dict[acl_name]['rules'][rule['sequence_num']] = rule + + return config_dict + + def sort_config(self, configs): + # natsort provides better result. + # The use of natsort causes sanity error due to it is not available in + # python version currently used. + # new_config = natsorted(new_config, key=lambda x: x['name']) + # For time-being, use simple "sort" + configs.sort(key=lambda x: x['name']) + + for conf in configs: + if conf.get('rules', []): + conf['rules'].sort(key=lambda x: x['sequence_num']) 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 index fccba7707..c47f06940 100644 --- 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 @@ -13,17 +13,19 @@ created from __future__ import absolute_import, division, print_function __metaclass__ = type -import json +import traceback 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, + get_ranges_in_list, update_states, normalize_interface_name ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_empties, to_list ) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import ( @@ -33,9 +35,14 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s to_request, edit_config ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG, + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) from ansible.module_utils._text import to_native from ansible.module_utils.connection import ConnectionError -import traceback LIB_IMP_ERR = None ERR_MSG = None @@ -47,6 +54,7 @@ except Exception as e: ERR_MSG = to_native(e) LIB_IMP_ERR = traceback.format_exc() +DELETE = 'delete' PATCH = 'patch' intf_key = 'openconfig-if-ethernet:ethernet' port_chnl_key = 'openconfig-if-aggregate:aggregation' @@ -54,6 +62,10 @@ port_chnl_key = 'openconfig-if-aggregate:aggregation' TEST_KEYS = [ {'allowed_vlans': {'vlan': ''}}, ] +TEST_KEYS_formatted_diff = [ + {'config': {'name': '', '__delete_op': __DELETE_CONFIG}}, + {'allowed_vlans': {'vlan': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] class L2_interfaces(ConfigBase): @@ -112,6 +124,19 @@ class L2_interfaces(ConfigBase): if result['changed']: result['after'] = changed_l2_interfaces_facts + new_config = changed_l2_interfaces_facts + old_config = existing_l2_interfaces_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_l2_interfaces_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + if self._module._diff: + self.sort_config(new_config) + self.sort_config(old_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -123,7 +148,15 @@ class L2_interfaces(ConfigBase): :returns: the commands necessary to migrate the current configuration to the desired configuration """ + state = self._module.params['state'] want = self._module.params['config'] + if want: + # In state deleted, specific empty parameters are supported + if state != 'deleted': + want = [remove_empties(conf) for conf in want] + else: + want = [] + normalize_interface_name(want, self._module) have = existing_l2_interfaces_facts @@ -147,45 +180,40 @@ class L2_interfaces(ConfigBase): """ state = self._module.params['state'] - diff = get_diff(want, have, TEST_KEYS) - if state == 'overridden': - commands, requests = self._state_overridden(want, have, diff) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': - commands, requests = self._state_merged(want, have, diff) + commands, requests = self._state_merged(want, have) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) return commands, requests - def _state_replaced(self, want, have, diff): + def _state_replaced(self, want, have): """ The command generator when state is replaced :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ - + commands = [] 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 = [] + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'replaced') + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests + + add_commands, add_requests = self.get_merge_commands_requests(want, have) + if add_commands: + commands.extend(update_states(add_commands, 'replaced')) + requests.extend(add_requests) return commands, requests - def _state_overridden(self, want, have, diff): + def _state_overridden(self, want, have): """ The command generator when state is overridden :rtype: A list @@ -195,23 +223,19 @@ class L2_interfaces(ConfigBase): 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) + del_commands, del_requests = self.get_delete_commands_requests_for_replaced_overridden(want, have, 'overridden') + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests - 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) + add_commands, add_requests = self.get_merge_commands_requests(want, have) + if add_commands: + commands.extend(update_states(add_commands, 'overridden')) + requests.extend(add_requests) return commands, requests - def _state_merged(self, want, have, diff): + def _state_merged(self, want, have): """ The command generator when state is merged :rtype: A list @@ -220,77 +244,235 @@ class L2_interfaces(ConfigBase): 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") + commands, requests = self.get_merge_commands_requests(want, have) + if commands: + commands = update_states(commands, 'merged') + return commands, requests - def _state_deleted(self, want, have, diff): + 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 = self.get_delete_commands_requests_for_deleted(want, have) + if commands: + commands = update_states(commands, 'deleted') - # 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) + return commands, requests + + def get_merge_commands_requests(self, want, have): + """Returns the commands and requests necessary to merge the provided + configurations into the current configuration + """ + commands = [] + requests = [] + if not want: + return commands, requests + + if have: + diff = get_diff(want, have, TEST_KEYS) else: - commands = want - requests = self.get_delete_specifig_switchport_requests(want, have) - if len(requests) == 0: - commands = [] + diff = want - if commands: - commands = update_states(commands, "deleted") + for cmd in diff: + name = cmd['name'] + if name == 'eth0': + continue + + if cmd.get('trunk') and cmd['trunk'].get('allowed_vlans'): + match = next((cnf for cnf in have if cnf['name'] == name), None) + if match: + cmd['trunk']['allowed_vlans'] = self.get_trunk_allowed_vlans_diff(cmd, match) + if not cmd['trunk']['allowed_vlans']: + cmd.pop('trunk') + + if cmd.get('access') or cmd.get('trunk'): + commands.append(cmd) + requests = self.get_create_l2_interface_requests(commands) return commands, requests - def get_trunk_delete_switchport_request(self, config, match_config): - method = "DELETE" - name = config['name'] + def get_delete_commands_requests_for_deleted(self, want, have): + """Returns the commands and requests necessary to remove the current + configuration of the provided objects when state is deleted + """ + commands = [] 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 + if not have: + return commands, requests + + if not want: + # Delete all L2 interface config + commands = [remove_empties(conf) for conf in have] + requests = self.get_delete_all_switchport_requests(commands) + return commands, requests + + for conf in want: + name = conf['name'] + matched = next((cnf for cnf in have if cnf['name'] == name), None) + if matched: + # If both access and trunk are not mentioned, delete all config + # in that interface + if not conf.get('access') and not conf.get('trunk'): + command = {'name': name} + if matched.get('access'): + command['access'] = matched['access'] + if matched.get('trunk'): + command['trunk'] = matched['trunk'] + + commands.append(command) + requests.extend(self.get_delete_all_switchport_requests([command])) + else: + command = {} + if conf.get('access'): + access_match = matched.get('access') + if conf['access'].get('vlan'): + if access_match and access_match.get('vlan') == conf['access']['vlan']: + command['access'] = {'vlan': conf['access']['vlan']} + requests.append(self.get_access_delete_switchport_request(name)) + else: + # If access -> vlan is mentioned without value, + # delete existing access vlan config + if access_match and access_match.get('vlan'): + command['access'] = {'vlan': access_match['vlan']} + requests.append(self.get_access_delete_switchport_request(name)) + + if conf.get('trunk'): + if conf['trunk'].get('allowed_vlans'): + trunk_vlans_to_delete = self.get_trunk_allowed_vlans_common(conf, matched) + if trunk_vlans_to_delete: + command['trunk'] = {'allowed_vlans': trunk_vlans_to_delete} + requests.append(self.get_trunk_allowed_vlans_delete_switchport_request(name, command['trunk']['allowed_vlans'])) + else: + # If trunk -> allowed_vlans is mentioned without + # value, delete existing trunk allowed vlans config + trunk_match = matched.get('trunk') + if trunk_match and trunk_match.get('allowed_vlans'): + command['trunk'] = {'allowed_vlans': trunk_match['allowed_vlans'].copy()} + requests.append(self.get_trunk_allowed_vlans_delete_switchport_request(name, command['trunk']['allowed_vlans'])) + + if command: + command['name'] = name + commands.append(command) + + return commands, requests + + def get_delete_commands_requests_for_replaced_overridden(self, want, have, state): + """Returns the commands and requests necessary to remove applicable + current configurations when state is replaced or overridden + """ + commands = [] + requests = [] + if not have: + return commands, requests + + have_interfaces = self.get_interface_names(have) + want_interfaces = self.get_interface_names(want) + interfaces_to_replace = have_interfaces.intersection(want_interfaces) + if state == 'overridden': + interfaces_to_delete = have_interfaces.difference(want_interfaces) + else: + interfaces_to_delete = [] + + if want: + del_diff = get_diff(have, want, TEST_KEYS) + else: + del_diff = have + + for conf in del_diff: + name = conf['name'] + + # Delete all config in interfaces not specified in overridden + if name in interfaces_to_delete: + command = {'name': name} + if conf.get('access'): + command['access'] = conf['access'] + if conf.get('trunk'): + command['trunk'] = conf['trunk'] + + commands.append(command) + requests.extend(self.get_delete_all_switchport_requests([command])) + + # Delete config in interfaces that are replaced/overridden + elif name in interfaces_to_replace: + command = {} + + if conf.get('access') and conf['access'].get('vlan'): + command['access'] = {'vlan': conf['access']['vlan']} + requests.append(self.get_access_delete_switchport_request(name)) + + if conf.get('trunk') and conf['trunk'].get('allowed_vlans'): + matched = next((cnf for cnf in want if cnf['name'] == name), None) + if matched: + trunk_vlans_to_delete = self.get_trunk_allowed_vlans_diff(conf, matched) + if trunk_vlans_to_delete: + command['trunk'] = {'allowed_vlans': trunk_vlans_to_delete} + requests.append(self.get_trunk_allowed_vlans_delete_switchport_request(name, command['trunk']['allowed_vlans'])) + + if command: + command['name'] = name + commands.append(command) + + return commands, requests + + def get_trunk_allowed_vlans_delete_switchport_request(self, intf_name, allowed_vlans): + """Returns the request as a dict to delete the trunk vlan ranges + specified in allowed_vlans for the given interface + """ + method = DELETE + vlan_id_list = "" + for each_allowed_vlan in allowed_vlans: + vlan_id = each_allowed_vlan['vlan'] + + if '-' in vlan_id: + vlan_id_fmt = vlan_id.replace('-', '..') + else: + vlan_id_fmt = vlan_id + + if vlan_id_list: + vlan_id_list += ",{0}".format(vlan_id_fmt) + else: + vlan_id_list = vlan_id_fmt + + key = intf_key + if intf_name.startswith('PortChannel'): + key = port_chnl_key + + url = "data/openconfig-interfaces:interfaces/interface={0}/{1}/".format(intf_name, key) + url += "openconfig-vlan:switched-vlan/config/" + url += "trunk-vlans=" + vlan_id_list.replace(',', '%2C') + + request = {"path": url, "method": method} + return request + + def get_access_delete_switchport_request(self, intf_name): + """Returns the request as a dict to delete the access vlan + configuration for the given interface + """ + method = DELETE + key = intf_key + if intf_name.startswith('PortChannel'): + key = port_chnl_key + url = "data/openconfig-interfaces:interfaces/interface={}/{}/openconfig-vlan:switched-vlan/config/access-vlan" + request = {"path": url.format(intf_name, key), "method": method} - 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): + """Returns a list of requests to delete all switchport + configuration for all interfaces specified in the config list + """ requests = [] if not configs: return requests # Create URL and payload url = "data/openconfig-interfaces:interfaces/interface={}/{}/openconfig-vlan:switched-vlan/config" - method = "DELETE" + method = DELETE for intf in configs: - name = intf.get("name") + name = intf['name'] key = intf_key if name.startswith('PortChannel'): key = port_chnl_key @@ -301,78 +483,19 @@ class L2_interfaces(ConfigBase): return requests - def get_delete_specifig_switchport_requests(self, configs, have): + def get_create_l2_interface_requests(self, configs): + """Returns a list of requests to add the switchport + configurations specified in the config list + """ 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" + method = PATCH for conf in configs: - name = conf.get('name') - if name == "eth0": - continue + name = conf['name'] key = intf_key if name.startswith('PortChannel'): key = port_chnl_key @@ -382,33 +505,124 @@ class L2_interfaces(ConfigBase): "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 + """Returns the payload to add the switchport configurations + specified in the interface config + """ + payload = {'openconfig-vlan:config': {}} + trunk_payload = [] + + if conf.get('access') and conf['access'].get('vlan'): + payload['openconfig-vlan:config']['access-vlan'] = int(conf['access']['vlan']) + + if conf.get('trunk') and conf['trunk'].get('allowed_vlans'): 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 + vlan_val = each_allowed_vlan['vlan'] + if '-' in vlan_val: + trunk_payload.append('{0}'.format(vlan_val.replace('-', '..'))) + else: + trunk_payload.append(int(vlan_val)) + + if trunk_payload: + payload['openconfig-vlan:config']['trunk-vlans'] = trunk_payload + + return payload + + def get_trunk_allowed_vlans_common(self, config, match): + """Returns the allowed vlan ranges that are common in the + interface configurations specified by 'config' and 'match' in + allowed_vlans spec format + """ + trunk_vlans = [] + match_trunk_vlans = [] + if config.get('trunk') and config['trunk'].get('allowed_vlans'): + trunk_vlans = config['trunk']['allowed_vlans'] + + if not trunk_vlans: + return [] + + if match.get('trunk') and match['trunk'].get('allowed_vlans'): + match_trunk_vlans = match['trunk']['allowed_vlans'] + + if not match_trunk_vlans: + return [] + + trunk_vlans = self.get_vlan_id_list(trunk_vlans) + match_trunk_vlans = self.get_vlan_id_list(match_trunk_vlans) + return self.get_allowed_vlan_range_list(list(set(trunk_vlans).intersection(set(match_trunk_vlans)))) + + def get_trunk_allowed_vlans_diff(self, config, match): + """Returns the allowed vlan ranges present only in 'config' + and and not in 'match' in allowed_vlans spec format + """ + trunk_vlans = [] + match_trunk_vlans = [] + if config.get('trunk') and config['trunk'].get('allowed_vlans'): + trunk_vlans = config['trunk']['allowed_vlans'] + + if not trunk_vlans: + return [] + + if match.get('trunk') and match['trunk'].get('allowed_vlans'): + match_trunk_vlans = match['trunk']['allowed_vlans'] + + if not match_trunk_vlans: + return trunk_vlans + + trunk_vlans = self.get_vlan_id_list(trunk_vlans) + match_trunk_vlans = self.get_vlan_id_list(match_trunk_vlans) + return self.get_allowed_vlan_range_list(list(set(trunk_vlans) - set(match_trunk_vlans))) + + @staticmethod + def get_vlan_id_list(allowed_vlan_range_list): + """Returns a list of all VLAN IDs specified in allowed_vlans list""" + vlan_id_list = [] + if allowed_vlan_range_list: + for vlan_range in allowed_vlan_range_list: + vlan_val = vlan_range['vlan'] + if '-' in vlan_val: + start, end = vlan_val.split('-') + vlan_id_list.extend(range(int(start), int(end) + 1)) + else: + # Single VLAN ID + vlan_id_list.append(int(vlan_val)) + + return vlan_id_list + + @staticmethod + def get_allowed_vlan_range_list(vlan_id_list): + """Returns the allowed_vlans list for given list of VLAN IDs""" + allowed_vlan_range_list = [] + + if vlan_id_list: + vlan_id_list.sort() + for vlan_range in get_ranges_in_list(vlan_id_list): + allowed_vlan_range_list.append({'vlan': '-'.join(map(str, (vlan_range[0], vlan_range[-1])[:len(vlan_range)]))}) + + return allowed_vlan_range_list + + @staticmethod + def get_interface_names(configs): + """Returns a set of interface names available in the given + configs list + """ + interface_names = set() + for conf in configs: + interface_names.add(conf['name']) + + return interface_names + + def sort_config(self, configs): + # natsort provides better result. + # The use of natsort causes sanity error due to it is not available in + # python version currently used. + # new_config = natsorted(new_config, key=lambda x: x['name']) + # For time-being, use simple "sort" + configs.sort(key=lambda x: x['name']) + + for conf in configs: + if conf.get('trunk', {}) and conf['trunk'].get('allowed_vlans', []): + conf['trunk']['allowed_vlans'].sort(key=lambda x: x['vlan']) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_acls/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_acls/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_acls/l3_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_acls/l3_acls.py new file mode 100644 index 000000000..26fbb7fdb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/l3_acls/l3_acls.py @@ -0,0 +1,763 @@ +# +# -*- 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_l3_acls 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 ast import literal_eval + +from ansible.module_utils._text import to_text +from ansible.module_utils.common.validation import check_required_arguments +from ansible.module_utils.connection import ConnectionError +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, + remove_empties, + validate_config +) +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 +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) + +DELETE = 'delete' +PATCH = 'patch' +POST = 'post' + +TEST_KEYS_formatted_diff = [ + {'config': {'address_family': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'acls': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'rules': {'sequence_num': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] + +L4_PORT_START = 0 +L4_PORT_END = 65535 + +protocol_number_to_name_map = { + 1: 'icmp', + 6: 'tcp', + 17: 'udp', + 58: 'icmpv6' +} +dscp_value_to_name_map = { + 0: 'default', + 8: 'cs1', + 16: 'cs2', + 24: 'cs3', + 32: 'cs4', + 40: 'cs5', + 48: 'cs6', + 56: 'cs7', + 10: 'af11', + 12: 'af12', + 14: 'af13', + 18: 'af21', + 20: 'af22', + 22: 'af23', + 26: 'af31', + 28: 'af32', + 30: 'af33', + 34: 'af41', + 36: 'af42', + 38: 'af43', + 46: 'ef', + 44: 'voice_admit' +} + +# Spec value to payload value mappings +acl_type_to_payload_map = { + 'ipv4': 'ACL_IPV4', + 'ipv6': 'ACL_IPV6' +} +acl_type_to_host_mask_map = { + 'ipv4': '/32', + 'ipv6': '/128' +} +action_value_to_payload_map = { + 'permit': 'ACCEPT', + 'discard': 'DISCARD', + 'do-not-nat': 'DO_NOT_NAT', + 'deny': 'DROP', + 'transit': 'TRANSIT' +} +protocol_name_to_payload_map = { + 'icmp': 'IP_ICMP', + 'icmpv6': 58, + 'tcp': 'IP_TCP', + 'udp': 'IP_UDP' +} +protocol_number_to_payload_map = { + 2: 'IP_IGMP', + 46: 'IP_RSVP', + 47: 'IP_GRE', + 51: 'IP_AUTH', + 103: 'IP_PIM', + 115: 'IP_L2TP' +} +dscp_name_to_value_map = {v: k for k, v in dscp_value_to_name_map.items()} + + +class L3_acls(ConfigBase): + """ + The sonic_l3_acls class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'l3_acls', + ] + + acl_path = 'data/openconfig-acl:acl/acl-sets/acl-set' + l3_acl_path = 'data/openconfig-acl:acl/acl-sets/acl-set={acl_name},{acl_type}' + l3_acl_rule_path = 'data/openconfig-acl:acl/acl-sets/acl-set={acl_name},{acl_type}/acl-entries' + l3_acl_remark_path = 'data/openconfig-acl:acl/acl-sets/acl-set={acl_name},{acl_type}/config/description' + + def __init__(self, module): + super(L3_acls, self).__init__(module) + + def get_l3_acls_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_acls_facts = facts['ansible_network_resources'].get('l3_acls') + if not l3_acls_facts: + return [] + return l3_acls_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + + existing_l3_acls_facts = self.get_l3_acls_facts() + commands, requests = self.set_config(existing_l3_acls_facts) + if commands: + if not self._module.check_mode: + try: + edit_config(self._module, to_request(self._module, requests)) + except ConnectionError as exc: + self._handle_failure_response(exc) + + result['changed'] = True + + changed_l3_acls_facts = self.get_l3_acls_facts() + + result['before'] = existing_l3_acls_facts + if result['changed']: + result['after'] = changed_l3_acls_facts + + result['commands'] = commands + + new_config = changed_l3_acls_facts + old_config = existing_l3_acls_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_l3_acls_facts, + TEST_KEYS_formatted_diff) + self.post_process_generated_config(new_config) + result['after(generated)'] = new_config + if self._module._diff: + self.sort_config(new_config) + self.sort_config(old_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) + result['warnings'] = warnings + return result + + def set_config(self, existing_l3_acls_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: + want = self.validate_and_normalize_config(want) + else: + want = [] + + have = existing_l3_acls_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 in ('merged', 'overridden', 'replaced'): + commands, requests = self._state_merged_overridden_replaced(want, have, state) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + + return commands, requests + + def _handle_failure_response(self, connection_error): + log = None + try: + response = literal_eval(connection_error.args[0]) + error_app_tag = response['ietf-restconf:errors']['error'][0].get('error-app-tag') + except Exception: + pass + else: + if error_app_tag == 'too-many-elements': + log = 'Exceeds maximum number of ACL / ACL Rules' + elif error_app_tag == 'update-not-allowed': + log = 'Creating ACLs with same name and different type not allowed' + + if log: + response.update({u'log': log}) + self._module.fail_json(msg=to_text(response), code=connection_error.code) + else: + self._module.fail_json(msg=str(connection_error), code=connection_error.code) + + def _state_merged_overridden_replaced(self, want, have, state): + """ The command generator when state is merged/overridden/replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + add_commands = [] + del_commands = [] + commands = [] + + add_requests = [] + del_requests = [] + requests = [] + + have_dict = self._convert_config_list_to_dict(have) + want_dict = self._convert_config_list_to_dict(want) + + for acl_type in ('ipv4', 'ipv6'): + acl_type_add_commands = [] + acl_type_del_commands = [] + + have_acl_names = set(have_dict.get(acl_type, {}).keys()) + want_acl_names = set(want_dict.get(acl_type, {}).keys()) + + if state == 'overridden': + # Delete non-modified ACLs + for acl_name in have_acl_names.difference(want_acl_names): + acl_type_del_commands.append({'name': acl_name}) + del_requests.append(self.get_delete_l3_acl_request(acl_type, acl_name)) + + # Modify existing ACLs + for acl_name in want_acl_names.intersection(have_acl_names): + acl_add_command = {'name': acl_name} + acl_del_command = {'name': acl_name} + rule_add_commands = [] + rule_del_commands = [] + + have_acl = have_dict[acl_type][acl_name] + want_acl = want_dict[acl_type][acl_name] + if not want_acl['remark']: + if have_acl['remark'] and state in ('replaced', 'overridden'): + acl_del_command['remark'] = have_acl['remark'] + del_requests.append(self.get_delete_l3_acl_remark_request(acl_type, acl_name)) + else: + if want_acl['remark'] != have_acl['remark']: + acl_add_command['remark'] = want_acl['remark'] + add_requests.append(self.get_create_l3_acl_remark_request(acl_type, acl_name, want_acl['remark'])) + + have_seq_nums = set(have_acl['rules'].keys()) + want_seq_nums = set(want_acl['rules'].keys()) + + if state in ('replaced', 'overridden'): + # Delete non-modified rules + for seq_num in have_seq_nums.difference(want_seq_nums): + rule_del_commands.append({'sequence_num': seq_num}) + del_requests.append(self.get_delete_l3_acl_rule_request(acl_type, acl_name, seq_num)) + + for seq_num in want_seq_nums.intersection(have_seq_nums): + # Replace existing rules + if have_acl['rules'][seq_num] != want_acl['rules'][seq_num]: + if state == 'merged': + self._module.fail_json( + msg="Cannot update existing sequence {0} of {1} ACL {2} with state merged." + " Please use state replaced or overridden.".format(seq_num, acl_type, acl_name) + ) + + rule_del_commands.append({'sequence_num': seq_num}) + del_requests.append(self.get_delete_l3_acl_rule_request(acl_type, acl_name, seq_num)) + + rule_add_commands.append(want_acl['rules'][seq_num]) + add_requests.append(self.get_create_l3_acl_rule_request(acl_type, acl_name, seq_num, want_acl['rules'][seq_num])) + + # Add new rules + for seq_num in want_seq_nums.difference(have_seq_nums): + rule_add_commands.append(want_acl['rules'][seq_num]) + add_requests.append(self.get_create_l3_acl_rule_request(acl_type, acl_name, seq_num, want_acl['rules'][seq_num])) + + if rule_del_commands: + acl_del_command['rules'] = rule_del_commands + if rule_add_commands: + acl_add_command['rules'] = rule_add_commands + + if acl_del_command.get('rules') or acl_del_command.get('remark'): + acl_type_del_commands.append(acl_del_command) + if acl_add_command.get('rules') or acl_add_command.get('remark'): + acl_type_add_commands.append(acl_add_command) + + # Add new ACLs + for acl_name in want_acl_names.difference(have_acl_names): + acl_add_command = {'name': acl_name} + add_requests.append(self.get_create_l3_acl_request(acl_type, acl_name)) + + want_acl = want_dict[acl_type][acl_name] + if want_acl['remark']: + acl_add_command['remark'] = want_acl['remark'] + add_requests.append(self.get_create_l3_acl_remark_request(acl_type, acl_name, want_acl['remark'])) + + # Add new rules + want_seq_nums = set(want_acl['rules'].keys()) + if want_seq_nums: + acl_add_command['rules'] = [] + for seq_num in want_seq_nums: + acl_add_command['rules'].append(want_acl['rules'][seq_num]) + add_requests.append(self.get_create_l3_acl_rule_request(acl_type, acl_name, seq_num, want_acl['rules'][seq_num])) + + acl_type_add_commands.append(acl_add_command) + + if acl_type_del_commands: + del_commands.append({'address_family': acl_type, 'acls': acl_type_del_commands}) + + if acl_type_add_commands: + add_commands.append({'address_family': acl_type, 'acls': acl_type_add_commands}) + + if del_commands: + commands = update_states(del_commands, 'deleted') + requests = del_requests + + if add_commands: + commands.extend(update_states(add_commands, state)) + requests.extend(add_requests) + + 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: + for config in have: + if not config.get('acls'): + continue + + acl_type_commands = [] + acl_type = config['address_family'] + for acl in config['acls']: + acl_type_commands.append({'name': acl['name']}) + requests.append(self.get_delete_l3_acl_request(acl_type, acl['name'])) + + if acl_type_commands: + commands.append({'address_family': acl_type, 'acls': acl_type_commands}) + else: + have_dict = self._convert_config_list_to_dict(have) + want_dict = self._convert_config_list_to_dict(want) + + for acl_type in ('ipv4', 'ipv6'): + acl_type_commands = [] + have_acl_names = set(have_dict.get(acl_type, {}).keys()) + want_acl_names = set(want_dict.get(acl_type, {}).keys()) + + # If only the type is specified, delete all ACLs of that type + if acl_type in want_dict and not want_acl_names: + for acl_name in have_acl_names: + acl_type_commands.append({'name': acl_name}) + requests.append(self.get_delete_l3_acl_request(acl_type, acl_name)) + + # Delete existing ACLs + for acl_name in want_acl_names.intersection(have_acl_names): + have_acl = have_dict[acl_type][acl_name] + want_acl = want_dict[acl_type][acl_name] + + # Delete entire ACL if only the name is specified + if not want_acl['remark'] and not want_acl['rules']: + acl_type_commands.append({'name': acl_name}) + requests.append(self.get_delete_l3_acl_request(acl_type, acl_name)) + continue + + acl_del_command = {'name': acl_name} + rule_del_commands = [] + have_seq_nums = set(have_acl['rules'].keys()) + want_seq_nums = set(want_acl['rules'].keys()) + + if want_acl['remark'] and want_acl['remark'] == have_acl['remark']: + acl_del_command['remark'] = want_acl['remark'] + requests.append(self.get_delete_l3_acl_remark_request(acl_type, acl_name)) + + # Delete existing rules + # When state is deleted, options other than sequence_num are not considered + for seq_num in want_seq_nums.intersection(have_seq_nums): + rule_del_commands.append({'sequence_num': seq_num}) + requests.append(self.get_delete_l3_acl_rule_request(acl_type, acl_name, seq_num)) + + if rule_del_commands: + acl_del_command['rules'] = rule_del_commands + + if acl_del_command.get('rules') or acl_del_command.get('remark'): + acl_type_commands.append(acl_del_command) + + if acl_type_commands: + commands.append({'address_family': acl_type, 'acls': acl_type_commands}) + + commands = update_states(commands, "deleted") + return commands, requests + + def get_create_l3_acl_request(self, acl_type, acl_name): + """Get request to create L3 ACL with specified type and name""" + url = self.acl_path + payload = { + 'acl-set': [{ + 'name': acl_name, + 'type': acl_type_to_payload_map[acl_type], + 'config': { + 'name': acl_name, + 'type': acl_type_to_payload_map[acl_type] + } + }] + } + + return {'path': url, 'method': PATCH, 'data': payload} + + def get_create_l3_acl_remark_request(self, acl_type, acl_name, remark): + """Get request to add given remark to the specified L3 ACL""" + url = self.l3_acl_remark_path.format(acl_name=acl_name, acl_type=acl_type_to_payload_map[acl_type]) + payload = {'description': remark} + return {'path': url, 'method': PATCH, 'data': payload} + + def get_create_l3_acl_rule_request(self, acl_type, acl_name, seq_num, rule): + """Get request to create a rule with given sequence number + and configuration in the specified L3 ACL + """ + url = self.l3_acl_rule_path.format(acl_name=acl_name, acl_type=acl_type_to_payload_map[acl_type]) + payload = { + 'openconfig-acl:acl-entry': [{ + 'sequence-id': seq_num, + 'config': { + 'sequence-id': seq_num + }, + acl_type: { + 'config': {} + }, + 'transport': { + 'config': {} + }, + 'actions': { + 'config': { + 'forwarding-action': action_value_to_payload_map[rule['action']] + } + } + }] + } + rule_l3_config = payload['openconfig-acl:acl-entry'][0][acl_type]['config'] + rule_l4_config = payload['openconfig-acl:acl-entry'][0]['transport']['config'] + + if rule['protocol'].get('number') is not None: + protocol = rule['protocol']['number'] + rule_l3_config['protocol'] = protocol_number_to_payload_map.get(protocol, protocol) + else: + protocol = rule['protocol']['name'] + if protocol not in ('ip', 'ipv6'): + rule_l3_config['protocol'] = protocol_name_to_payload_map[protocol] + + if rule['source'].get('host'): + rule_l3_config['source-address'] = rule['source']['host'] + acl_type_to_host_mask_map[acl_type] + elif rule['source'].get('prefix'): + rule_l3_config['source-address'] = rule['source']['prefix'] + + src_port_number = self._convert_port_dict_to_payload_format(rule['source'].get('port_number')) + if src_port_number: + rule_l4_config['source-port'] = src_port_number + + if rule['destination'].get('host'): + rule_l3_config['destination-address'] = rule['destination']['host'] + acl_type_to_host_mask_map[acl_type] + elif rule['destination'].get('prefix'): + rule_l3_config['destination-address'] = rule['destination']['prefix'] + + dest_port_number = self._convert_port_dict_to_payload_format(rule['destination'].get('port_number')) + if dest_port_number: + rule_l4_config['destination-port'] = dest_port_number + + if rule.get('protocol_options'): + if protocol in ('icmp', 'icmpv6') and rule['protocol_options'].get(protocol): + if rule['protocol_options'][protocol].get('type') is not None: + rule_l4_config['icmp-type'] = rule['protocol_options'][protocol]['type'] + if rule['protocol_options'][protocol].get('code') is not None: + rule_l4_config['icmp-code'] = rule['protocol_options'][protocol]['code'] + elif rule['protocol_options'].get('tcp'): + if rule['protocol_options']['tcp'].get('established'): + rule_l4_config['tcp-session-established'] = True + else: + tcp_flag_list = [] + for tcp_flag in rule['protocol_options']['tcp'].keys(): + if rule['protocol_options']['tcp'][tcp_flag]: + tcp_flag_list.append('tcp_{0}'.format(tcp_flag).upper()) + + if tcp_flag_list: + rule_l4_config['tcp-flags'] = tcp_flag_list + + if rule.get('vlan_id') is not None: + payload['openconfig-acl:acl-entry'][0]['l2'] = { + 'config': { + 'vlanid': rule['vlan_id'] + } + } + + if rule.get('dscp'): + if rule['dscp'].get('value') is not None: + rule_l3_config['dscp'] = rule['dscp']['value'] + else: + dscp_opt = next(iter(rule['dscp'])) + if rule['dscp'][dscp_opt]: + rule_l3_config['dscp'] = dscp_name_to_value_map[dscp_opt] + + if rule.get('remark'): + payload['openconfig-acl:acl-entry'][0]['config']['description'] = rule['remark'] + + return {'path': url, 'method': POST, 'data': payload} + + def get_delete_l3_acl_request(self, acl_type, acl_name): + """Get request to delete L3 ACL with specified type and name""" + url = self.l3_acl_path.format(acl_name=acl_name, acl_type=acl_type_to_payload_map[acl_type]) + return {'path': url, 'method': DELETE} + + def get_delete_l3_acl_remark_request(self, acl_type, acl_name): + """Get request to delete remark of the specified L3 ACL""" + url = self.l3_acl_remark_path.format(acl_name=acl_name, acl_type=acl_type_to_payload_map[acl_type]) + return {'path': url, 'method': DELETE} + + def get_delete_l3_acl_rule_request(self, acl_type, acl_name, seq_num): + """Get request to delete the rule with given sequence number + in the specified L3 ACL + """ + url = self.l3_acl_rule_path.format(acl_name=acl_name, acl_type=acl_type_to_payload_map[acl_type]) + url += '/acl-entry={0}'.format(seq_num) + return {'path': url, 'method': DELETE} + + def validate_and_normalize_config(self, config_list): + """Validate and normalize the given config""" + # Remove empties and validate the config with argument spec + updated_config_list = [remove_empties(config) for config in config_list] + validate_config(self._module.argument_spec, {'config': updated_config_list}) + + state = self._module.params['state'] + # When state is deleted, options other than sequence_num are not considered + if state == 'deleted': + return updated_config_list + + for config in updated_config_list: + if not config.get('acls'): + continue + + acl_type = config['address_family'] + for acl in config['acls']: + if not acl.get('rules'): + continue + + acl_name = acl['name'] + for rule in acl['rules']: + seq_num = rule['sequence_num'] + + self._check_required(['action', 'source', 'destination', 'protocol'], rule, ['config', 'acls', 'rules']) + self._validate_and_normalize_protocol(acl_type, acl_name, rule) + protocol = rule['protocol']['name'] if rule['protocol'].get('name') else str(rule['protocol']['number']) + + for endpoint in ('source', 'destination'): + if rule[endpoint].get('any') is False: + self._invalid_rule('True is the only valid value for {0} -> any'.format(endpoint), acl_type, acl_name, seq_num) + elif rule[endpoint].get('host'): + rule[endpoint]['host'] = rule[endpoint]['host'].lower() + elif rule[endpoint].get('prefix'): + rule[endpoint]['prefix'] = rule[endpoint]['prefix'].lower() + + if rule[endpoint].get('port_number'): + if protocol not in ('tcp', 'udp'): + self._invalid_rule('{0} -> port_number is valid only for TCP or UDP protocol'.format(endpoint), acl_type, acl_name, seq_num) + + self._validate_and_normalize_port_number(acl_type, acl_name, rule, endpoint) + + if rule.get('protocol_options'): + protocol_options = next(iter(rule['protocol_options'])) + if protocol != protocol_options: + self._invalid_rule('protocol_options -> {0} is not valid for protocol {1}'.format(protocol_options, protocol), + acl_type, acl_name, seq_num) + + self._normalize_protocol_options(rule) + + self._normalize_dscp(rule) + + return updated_config_list + + def _validate_and_normalize_protocol(self, acl_type, acl_name, rule): + protocol = rule.get('protocol') + if protocol: + if protocol.get('number') is not None: + if protocol['number'] in protocol_number_to_name_map: + protocol['name'] = protocol_number_to_name_map[protocol.pop('number')] + + protocol_name = protocol.get('name') + if (acl_type == 'ipv4' and protocol_name in ('ipv6', 'icmpv6')) or (acl_type == 'ipv6' and protocol_name in ('ip', 'icmp')): + self._invalid_rule('invalid protocol {0} for {1} ACL'.format(protocol_name, acl_type), acl_type, acl_name, rule['sequence_num']) + + def _validate_and_normalize_port_number(self, acl_type, acl_name, rule, endpoint): + port_number = rule.get(endpoint, {}).get('port_number') + if port_number: + # Greater than 0 is the same as less than 65535 + if port_number.get('gt') == L4_PORT_START: + port_number['lt'] = L4_PORT_END + del port_number['gt'] + elif rule[endpoint]['port_number'].get('range'): + port_range = rule[endpoint]['port_number']['range'] + if port_range['begin'] >= port_range['end']: + self._invalid_rule('begin must be less than end in {0} -> port_number -> range'.format(endpoint), acl_type, acl_name, rule['sequence_num']) + + # Range of 0 to x is the same as less than x and + # range of x to 65535 is the same as greater than x + if port_range['begin'] == L4_PORT_START: + port_number['lt'] = port_range['end'] + del port_number['range'] + elif port_range['end'] == L4_PORT_END: + port_number['gt'] = port_range['begin'] + del port_number['range'] + + def _invalid_rule(self, err_msg, acl_type, acl_name, seq_num): + self._module.fail_json(msg='{0} ACL {1}, sequence number {2}: {3}'.format(acl_type, acl_name, seq_num, err_msg)) + + def _check_required(self, required_parameters, parameters, options_context=None): + if required_parameters: + spec = {} + for parameter in required_parameters: + spec[parameter] = {'required': True} + + try: + check_required_arguments(spec, parameters, options_context) + except TypeError as exc: + self._module.fail_json(msg=str(exc)) + + @staticmethod + def _normalize_protocol_options(rule): + tcp = rule.get('protocol_options', {}).get('tcp') + if tcp: + # Remove protocol_options option if all tcp options are False + if not any(list(tcp.values())): + del rule['protocol_options'] + else: + tcp_flag_list = list(tcp.keys()) + for tcp_flag in tcp_flag_list: + # Remove tcp option if its value is False + if not tcp[tcp_flag]: + del tcp[tcp_flag] + + @staticmethod + def _normalize_dscp(rule): + dscp = rule.get('dscp') + if dscp: + if dscp.get('value') is not None: + if dscp['value'] in dscp_value_to_name_map: + dscp[dscp_value_to_name_map[dscp.pop('value')]] = True + else: + # Remove dscp option if its value is False + if not next(iter(dscp.values())): + del rule['dscp'] + + @staticmethod + def _convert_config_list_to_dict(config_list): + config_dict = {} + for config in config_list: + acl_type = config['address_family'] + config_dict[acl_type] = {} + if config.get('acls'): + for acl in config['acls']: + acl_name = acl['name'] + config_dict[acl_type][acl_name] = {} + config_dict[acl_type][acl_name]['remark'] = acl.get('remark') + config_dict[acl_type][acl_name]['rules'] = {} + if acl.get('rules'): + for rule in acl['rules']: + config_dict[acl_type][acl_name]['rules'][rule['sequence_num']] = rule + + return config_dict + + @staticmethod + def _convert_port_dict_to_payload_format(port_dict): + payload = None + if port_dict: + if port_dict.get('eq') is not None: + payload = port_dict['eq'] + elif port_dict.get('lt') is not None: + payload = '{0}..{1}'.format(L4_PORT_START, port_dict['lt']) + elif port_dict.get('gt') is not None: + payload = '{0}..{1}'.format(port_dict['gt'], L4_PORT_END) + elif port_dict.get('range'): + payload = '{0}..{1}'.format(port_dict['range']['begin'], port_dict['range']['end']) + + return payload + + def sort_config(self, configs): + # natsort provides better result. + # The use of natsort causes sanity error due to it is not available in + # python version currently used. + # new_config = natsorted(new_config, key=lambda x: x['name']) + # For time-being, use simple "sort" + configs.sort(key=lambda x: x['address_family']) + + for conf in configs: + acls = conf.get('acls', []) + if acls: + acls.sort(key=lambda x: x['name']) + for acl in acls: + if acl.get('rules', []): + acl['rules'].sort(key=lambda x: x['sequence_num']) + + def post_process_generated_config(self, configs): + for conf in configs[:]: + if not conf.get('acls', []): + configs.remove(conf) 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 index d1b735251..200d8552b 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -30,7 +30,6 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s to_request, edit_config ) -from ansible.module_utils._text import to_native from ansible.module_utils.connection import ConnectionError TEST_KEYS = [ @@ -125,17 +124,17 @@ class L3_interfaces(ConfigBase): state = self._module.params['state'] diff = get_diff(want, have, TEST_KEYS) if state == 'overridden': - commands, requests = self._state_overridden(want, have, diff) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': - commands, requests = self._state_deleted(want, have, diff) + commands, requests = self._state_deleted(want, have) elif state == 'merged': commands, requests = self._state_merged(want, have, diff) elif state == 'replaced': - commands, requests = self._state_replaced(want, have, diff) + commands, requests = self._state_replaced(want, have) ret_commands = commands return ret_commands, requests - def _state_replaced(self, want, have, diff): + def _state_replaced(self, want, have): """ The command generator when state is replaced :rtype: A list @@ -144,19 +143,22 @@ class L3_interfaces(ConfigBase): """ 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) + new_want = self.update_object(want) + new_have = self.remove_default_entries(have) + get_replace_interfaces_list = self.get_interface_object_for_replaced(new_have, want) + + diff = get_diff(get_replace_interfaces_list, new_want, TEST_KEYS) + if diff: - delete_l3_interfaces_requests = self.get_delete_all_requests(want) + delete_l3_interfaces_requests = self.get_delete_all_requests(diff) ret_requests.extend(delete_l3_interfaces_requests) - commands.extend(update_states(want, "deleted")) + commands.extend(update_states(diff, "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")) + commands.extend(update_states(want, "replaced")) return commands, ret_requests - def _state_overridden(self, want, have, diff): + def _state_overridden(self, want, have): """ The command generator when state is overridden :rtype: A list @@ -165,16 +167,19 @@ class L3_interfaces(ConfigBase): """ 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) + new_want = self.update_object(want) + new_have = self.remove_default_entries(have) + get_override_interfaces = self.get_interface_object_for_overridden(new_have) + diff = get_diff(get_override_interfaces, new_want, TEST_KEYS) + diff2 = get_diff(new_want, get_override_interfaces, TEST_KEYS) + + if diff or diff2: + delete_interfaces_requests = self.get_delete_all_requests(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) + commands.extend(update_states(diff, "deleted")) + interfaces_to_create_requests = self.get_create_l3_interfaces_requests(want, have, want) ret_requests.extend(interfaces_to_create_requests) - commands.extend(update_states(diff, "merged")) + commands.extend(update_states(want, "overridden")) return commands, ret_requests @@ -195,7 +200,7 @@ class L3_interfaces(ConfigBase): return commands, requests - def _state_deleted(self, want, have, diff): + def _state_deleted(self, want, have): """ The command generator when state is deleted :rtype: A list @@ -215,7 +220,16 @@ class L3_interfaces(ConfigBase): commands = update_states(commands, "deleted") return commands, requests - def get_object(self, have, want): + def remove_default_entries(self, have): + new_have = list() + for obj in have: + if obj['ipv4']['addresses'] is not None or obj['ipv4']['anycast_addresses'] is not None: + new_have.append(obj) + elif obj['ipv6']['addresses'] is not None or obj['ipv6']['enabled']: + new_have.append(obj) + return new_have + + def get_interface_object_for_replaced(self, have, want): objects = list() names = [i.get('name', None) for i in want] for obj in have: @@ -223,6 +237,43 @@ class L3_interfaces(ConfigBase): objects.append(obj.copy()) return objects + def update_object(self, want): + objects = list() + for obj in want: + new_obj = {} + if 'name' in obj: + new_obj['name'] = obj['name'] + if obj['ipv4'] is None: + new_obj['ipv4'] = {'addresses': None, 'anycast_addresses': None} + else: + new_obj['ipv4'] = obj['ipv4'] + + if obj['ipv6'] is None: + new_obj['ipv6'] = {'addresses': None, 'enabled': False} + else: + new_obj['ipv6'] = obj['ipv6'] + + objects.append(new_obj) + return objects + + def get_interface_object_for_overridden(self, have): + objects = list() + for obj in have: + if 'name' in obj and obj['name'] != "Management0": + ipv4_addresses = obj['ipv4']['addresses'] + ipv6_addresses = obj['ipv6']['addresses'] + anycast_addresses = obj['ipv4']['anycast_addresses'] + ipv6_enable = obj['ipv6']['enabled'] + + if ipv4_addresses is not None or ipv6_addresses is not None: + objects.append(obj.copy()) + continue + + if ipv6_enable or anycast_addresses is not None: + objects.append(obj.copy()) + continue + return objects + def get_address(self, ip_str, have_obj): to_return = list() for i in have_obj: @@ -241,6 +292,8 @@ class L3_interfaces(ConfigBase): 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' + if not want: + return requests for each_l3 in want: l3 = each_l3.copy() name = l3.pop('name') @@ -274,7 +327,7 @@ class L3_interfaces(ConfigBase): 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'): + elif ipv4 and not 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 @@ -299,24 +352,27 @@ class L3_interfaces(ConfigBase): # 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 ipv4_addrs: + for ip in ipv4_addrs: + if have_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) + + if ipv4_anycast_addrs: + for ip in ipv4_anycast_addrs: + if have_ipv4_anycast_addrs and 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) if is_del_ipv6: if have_ipv6_addrs and len(have_ipv6_addrs) != 0: @@ -334,12 +390,12 @@ class L3_interfaces(ConfigBase): 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 ipv6_addrs: + 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} @@ -349,8 +405,9 @@ class L3_interfaces(ConfigBase): 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) + if l3['name'] != "Management0": + 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): @@ -364,6 +421,8 @@ class L3_interfaces(ConfigBase): name = l3.get('name') ipv4_addrs = [] ipv4_anycast = [] + if name == "Management0": + continue if l3.get('ipv4'): if l3['ipv4'].get('addresses'): ipv4_addrs = l3['ipv4']['addresses'] 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 index 541de2c4c..7ccd8ce02 100644 --- 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 @@ -21,6 +21,9 @@ except ImportError: import json +from copy import ( + deepcopy +) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -39,6 +42,11 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s to_request, edit_config ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) from ansible.module_utils._text import to_native from ansible.module_utils.connection import ConnectionError import traceback @@ -60,6 +68,10 @@ DELETE = 'delete' TEST_KEYS = [ {'interfaces': {'member': ''}}, ] +TEST_KEYS_formatted_diff = [ + {'config': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'interfaces': {'member': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] class Lag_interfaces(ConfigBase): @@ -119,6 +131,19 @@ class Lag_interfaces(ConfigBase): if result['changed']: result['after'] = changed_lag_interfaces_facts + new_config = changed_lag_interfaces_facts + old_config = existing_lag_interfaces_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_lag_interfaces_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + if self._module._diff: + self.sort_config(new_config) + self.sort_config(old_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -188,7 +213,7 @@ class Lag_interfaces(ConfigBase): replaced_list.append(list_obj) requests = self.get_delete_lag_interfaces_requests(replaced_list) if requests: - commands.extend(update_states(replaced_list, "replaced")) + commands.extend(update_states(replaced_list, "deleted")) replaced_commands, replaced_requests = self.template_for_lag_creation(have, diff_members, diff_portchannels, "replaced") if replaced_requests: commands.extend(replaced_commands) @@ -208,20 +233,31 @@ class Lag_interfaces(ConfigBase): 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) + commands.extend(update_states(replaced_list, "deleted")) + + deleted_po_list = list() + for i in delete_list: + list_obj = search_obj_in_list(i['name'], want, "name") + if not list_obj: + deleted_po_list.append(i) + + requests_deleted_po = self.get_delete_portchannel_requests(deleted_po_list) + requests.extend(requests_deleted_po) + commands_del = self.prune_commands(deleted_po_list) + commands.extend(update_states(commands_del, "deleted")) + 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): @@ -248,7 +284,8 @@ class Lag_interfaces(ConfigBase): 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")) + commands_del = self.prune_commands(have) + commands.extend(update_states(commands_del, "deleted")) else: # delete specific lag interfaces and specific portchannels commands = get_diff(want, diff, TEST_KEYS) commands = remove_empties_from_list(commands) @@ -312,7 +349,8 @@ class Lag_interfaces(ConfigBase): 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)) + commands_del = self.prune_commands(delete_portchannels) + commands.extend(update_states(commands_del, state_name)) if requests: requests.extend(portchannel_requests) else: @@ -336,8 +374,7 @@ class Lag_interfaces(ConfigBase): 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} + input_data = {"name": name} env = jinja2.Environment(autoescape=False) t = env.from_string(payload_template) intended_payload = t.render(input_data) @@ -419,3 +456,21 @@ class Lag_interfaces(ConfigBase): requests.append(request) return requests + + def sort_config(self, configs): + # natsort provides better result. + # The use of natsort causes sanity error due to it is not available in + # python version currently used. + # new_config = natsorted(new_config, key=lambda x: x['name']) + # For time-being, use simple "sort" + configs.sort(key=lambda x: x['name']) + + for conf in configs: + if conf.get('members', {}) and conf['members'].get('interfaces', []): + conf['members']['interfaces'].sort(key=lambda x: x['member']) + + def prune_commands(self, commands): + cmds = deepcopy(commands) + for cmd in cmds: + cmd.pop('members', None) + return cmds diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lldp_global/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lldp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lldp_global/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lldp_global/lldp_global.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lldp_global/lldp_global.py new file mode 100644 index 000000000..f27d63e81 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/lldp_global/lldp_global.py @@ -0,0 +1,296 @@ +# +# -*- 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_lldp_global class +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to it's desired end-state is +created +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( + ConfigBase, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_list, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import Facts + +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_diff, + update_states +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + + +PATCH = 'patch' +DELETE = 'delete' + + +class Lldp_global(ConfigBase): + """ + The sonic_lldp_global class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'lldp_global', + ] + + lldp_global_path = 'data/openconfig-lldp:lldp/config' + lldp_global_config_path = { + 'enable': lldp_global_path + '/enabled', + 'hello_time': lldp_global_path + '/hello-timer', + 'mode': lldp_global_path + '/openconfig-lldp-ext:mode', + 'multiplier': lldp_global_path + '/openconfig-lldp-ext:multiplier', + 'system_description': lldp_global_path + '/system-description', + 'system_name': lldp_global_path + '/system-name', + 'tlv_select': lldp_global_path + '/suppress-tlv-advertisement', + } + lldp_suppress_tlv = '/data/openconfig-lldp:lldp/config/suppress-tlv-advertisement={lldp_suppress_tlv}' + + def __init__(self, module): + super(Lldp_global, self).__init__(module) + + def get_lldp_global_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) + lldp_global_facts = facts['ansible_network_resources'].get('lldp_global') + if not lldp_global_facts: + return [] + return lldp_global_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + + existing_lldp_global_facts = self.get_lldp_global_facts() + commands, requests = self.set_config(existing_lldp_global_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 + + changed_lldp_global_facts = self.get_lldp_global_facts() + + result['before'] = existing_lldp_global_facts + if result['changed']: + result['after'] = changed_lldp_global_facts + + result['commands'] = commands + result['warnings'] = warnings + return result + + def set_config(self, existing_lldp_global_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_lldp_global_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) + if state == 'deleted': + commands, requests = self._state_deleted(want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(diff) + return 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 = [] + requests.extend(self.get_modify_specific_lldp_global_param_requests(commands)) + 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 = [] + requests = [] + + if not want: + commands = have + requests.extend(self.get_delete_lldp_global_completely_requests(commands)) + else: + commands = get_diff(want, diff) + requests.extend(self.get_delete_specific_lldp_global_param_requests(commands, have)) + + if len(requests) == 0: + commands = [] + + if commands: + commands = update_states(commands, "deleted") + + return commands, requests + + def get_modify_specific_lldp_global_param_requests(self, command): + """Get requests to modify specific LLDP Global configurations + based on the command specified for the interface + """ + requests = [] + + if not command: + return requests + if 'enable' in command and command['enable'] is not None: + payload = {'openconfig-lldp:enabled': command['enable']} + url = self.lldp_global_config_path['enable'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if 'hello_time' in command and command['hello_time'] is not None: + payload = {'openconfig-lldp:hello-timer': str(command['hello_time'])} + url = self.lldp_global_config_path['hello_time'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if 'mode' in command and command['mode'] is not None: + payload = {'openconfig-lldp-ext:mode': command['mode'].upper()} + url = self.lldp_global_config_path['mode'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if 'multiplier' in command and command['multiplier'] is not None: + payload = {'openconfig-lldp-ext:multiplier': int(command['multiplier'])} + url = self.lldp_global_config_path['multiplier'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if 'system_name' in command and command['system_name'] is not None: + payload = {'openconfig-lldp:system-name': command['system_name']} + url = self.lldp_global_config_path['system_name'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if 'system_description' in command and command['system_description'] is not None: + payload = {'openconfig-lldp:system-description': command['system_description']} + url = self.lldp_global_config_path['system_description'] + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + if 'tlv_select' in command: + if 'management_address' in command['tlv_select']: + payload = {'openconfig-lldp:suppress-tlv-advertisement': ["MANAGEMENT_ADDRESS"]} + url = self.lldp_global_config_path['tlv_select'] + if command['tlv_select']['management_address'] is False: + requests.append({'path': url, 'method': PATCH, 'data': payload}) + elif command['tlv_select']['management_address'] is True: + url = self.lldp_suppress_tlv.format(lldp_suppress_tlv="MANAGEMENT_ADDRESS") + requests.append({'path': url, 'method': DELETE}) + if 'system_capabilities' in command['tlv_select']: + payload = {'openconfig-lldp:suppress-tlv-advertisement': ["SYSTEM_CAPABILITIES"]} + url = self.lldp_global_config_path['tlv_select'] + if command['tlv_select']['system_capabilities'] is False: + requests.append({'path': url, 'method': PATCH, 'data': payload}) + elif command['tlv_select']['system_capabilities'] is True: + url = self.lldp_suppress_tlv.format(lldp_suppress_tlv="SYSTEM_CAPABILITIES") + requests.append({'path': url, 'method': DELETE}) + return requests + + def get_delete_lldp_global_completely_requests(self, have): + """Get requests to delete all existing LLDP global + configurations in the chassis + """ + default_config_dict = {"enable": True, "tlv_select": {"management_address": True, "system_capabilities": True}} + requests = [] + if default_config_dict != have: + return [{'path': self.lldp_global_path, 'method': DELETE}] + return requests + + def get_delete_specific_lldp_global_param_requests(self, command, config): + """Get requests to delete specific LLDP global configurations + based on the command specified for the interface + """ + requests = [] + + if not command: + return requests + if 'hello_time' in command: + url = self.lldp_global_config_path['hello_time'] + requests.append({'path': url, 'method': DELETE}) + + if 'enable' in command: + url = self.lldp_global_config_path['enable'] + if command['enable'] is False: + payload = {'openconfig-lldp:enabled': True} + elif command['enable'] is True: + payload = {'openconfig-lldp:enabled': False} + requests.append({'path': url, 'method': PATCH, 'data': payload}) + if 'mode' in command: + url = self.lldp_global_config_path['mode'] + requests.append({'path': url, 'method': DELETE}) + + if 'multiplier' in command: + url = self.lldp_global_config_path['multiplier'] + requests.append({'path': url, 'method': DELETE}) + + if 'system_name' in command: + url = self.lldp_global_config_path['system_name'] + requests.append({'path': url, 'method': DELETE}) + + if 'system_description' in command: + url = self.lldp_global_config_path['system_description'] + requests.append({'path': url, 'method': DELETE}) + # The tlv_select configs are enabled by default.Hence false leads deletion of configs. + if 'tlv_select' in command: + if 'management_address' in command['tlv_select']: + payload = {'openconfig-lldp:suppress-tlv-advertisement': ["MANAGEMENT_ADDRESS"]} + url = self.lldp_global_config_path['tlv_select'] + if command['tlv_select']['management_address'] is True: + requests.append({'path': url, 'method': PATCH, 'data': payload}) + elif command['tlv_select']['management_address'] is False: + url = self.lldp_suppress_tlv.format(lldp_suppress_tlv="MANAGEMENT_ADDRESS") + requests.append({'path': url, 'method': DELETE}) + if 'system_capabilities' in command['tlv_select']: + payload = {'openconfig-lldp:suppress-tlv-advertisement': ["SYSTEM_CAPABILITIES"]} + url = self.lldp_global_config_path['tlv_select'] + if command['tlv_select']['system_capabilities'] is True: + requests.append({'path': url, 'method': PATCH, 'data': payload}) + elif command['tlv_select']['system_capabilities'] is False: + url = self.lldp_suppress_tlv.format(lldp_suppress_tlv="SYSTEM_CAPABILITIES") + requests.append({'path': url, 'method': DELETE}) + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/logging/logging.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/logging/logging.py new file mode 100644 index 000000000..82262b561 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/logging/logging.py @@ -0,0 +1,458 @@ +# +# -*- 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_logging 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, + get_normalize_interface_name, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) +from ansible.module_utils.connection import ConnectionError + +PATCH = 'PATCH' +DELETE = 'DELETE' + +DEFAULT_REMOTE_PORT = 514 +DEFAULT_LOG_TYPE = 'log' + +TEST_KEYS = [ + { + "remote_servers": {"host": ""} + } +] +TEST_KEYS_formatted_diff = [ + { + "remote_servers": {"host": "", '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG} + } +] + + +class Logging(ConfigBase): + """ + The sonic_logging class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'logging', + ] + + def __init__(self, module): + super(Logging, self).__init__(module) + + def get_logging_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) + logging_facts = facts['ansible_network_resources'].get('logging') + if not logging_facts: + return [] + return logging_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_logging_facts = self.get_logging_facts() + + commands, requests = self.set_config(existing_logging_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_logging_facts = self.get_logging_facts() + + result['before'] = existing_logging_facts + if result['changed']: + result['after'] = changed_logging_facts + + new_config = changed_logging_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_logging_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_logging_facts, + new_config, + self._module._verbosity) + result['warnings'] = warnings + return result + + def set_config(self, existing_logging_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_logging_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) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(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 + """ + # Get a list of requested servers to delete that are not present in the current + # configuration on the device. This list can be used to filter out these + # unconfigured servers from the list of "delete" commands to be sent to the switch. + unconfigured = get_diff(want, have, TEST_KEYS) + + want_none = {'remote_servers': 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 unconfigured: + commands = want_any + else: + # Some of the servers requested for deletion are not in the current + # device configuration. Filter these out of the list to be used for sending + # "delete" commands to the device. + commands = get_diff(want_any, unconfigured, 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 _state_replaced(self, want, have): + """ The command generator when state is replaced + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + replaced_config = self.get_replaced_config(have, want) + if 'remote_servers' in replaced_config: + replaced_config['remote_servers'].sort(key=self.get_host) + if 'remote_servers' in want: + want['remote_servers'].sort(key=self.get_host) + + if replaced_config and replaced_config != want: + delete_all = False + del_requests = self.get_delete_requests(replaced_config, delete_all) + requests.extend(del_requests) + commands.extend(update_states(replaced_config, "deleted")) + replaced_config = [] + + if not replaced_config and want: + add_commands = want + add_requests = self.get_merge_requests(add_commands, replaced_config) + + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "replaced")) + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + if 'remote_servers' in have: + have['remote_servers'].sort(key=self.get_host) + if 'remote_servers' in want: + want['remote_servers'].sort(key=self.get_host) + + commands = [] + requests = [] + + if have and have != want: + delete_all = True + del_requests = self.get_delete_requests(have, delete_all) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + add_commands = want + add_requests = self.get_merge_requests(add_commands, have) + + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "overridden")) + + return commands, requests + + def get_host(self, remote_server): + return remote_server.get('host') + + def search_config_servers(self, host, servers): + + if servers is not None: + for server in servers: + if server['host'] == host: + return server + return [] + + def get_replaced_config(self, have, want): + + replaced_config = dict() + replaced_servers = [] + if 'remote_servers' in have and 'remote_servers' in want: + for server in want['remote_servers']: + replaced_server = self.search_config_servers(server['host'], have['remote_servers']) + if replaced_server: + replaced_servers.append(replaced_server) + + replaced_config['remote_servers'] = replaced_servers + return replaced_config + + def validate_want(self, want, state): + + if state == 'deleted': + + if 'remote_servers' in want and want['remote_servers'] is not None: + for server in want['remote_servers']: + source_interface_config = server.get('source_interface', None) + remote_port_config = server.get('remote_port', None) + message_type_config = server.get('message_type', None) + vrf_config = server.get('vrf', None) + if source_interface_config or remote_port_config or \ + message_type_config or vrf_config: + err_msg = "Logging remote_server parameter(s) can not be deleted." + self._module.fail_json(msg=err_msg, code=405) + + def preprocess_want(self, want, state): + + if state == 'merged': + if 'remote_servers' in want and want['remote_servers'] is not None: + for server in want['remote_servers']: + if 'source_interface' in server and not server['source_interface']: + server.pop('source_interface', None) + else: + server['source_interface'] = \ + get_normalize_interface_name(server['source_interface'], self._module) + if 'remote_port' in server and not server['remote_port']: + server.pop('remote_port', None) + if 'message_type' in server and not server['message_type']: + server.pop('message_type', None) + if 'vrf' in server and not server['vrf']: + server.pop('vrf', None) + + if state == 'replaced' or state == 'overridden': + if 'remote_servers' in want and want['remote_servers'] is not None: + for server in want['remote_servers']: + if 'source_interface' in server and not server['source_interface']: + server.pop('source_interface', None) + else: + server['source_interface'] = \ + get_normalize_interface_name(server['source_interface'], self._module) + if 'remote_port' in server and not server['remote_port']: + server['remote_port'] = DEFAULT_REMOTE_PORT + if 'message_type' in server and not server['message_type']: + server['message_type'] = DEFAULT_LOG_TYPE + + def get_merge_requests(self, configs, have): + + requests = [] + + servers_config = configs.get('remote_servers', None) + if servers_config: + servers_request = self.get_create_servers_requests(servers_config, have) + if servers_request: + requests.extend(servers_request) + + return requests + + def get_delete_requests(self, configs, delete_all): + + requests = [] + + servers_config = configs.get('remote_servers', None) + if servers_config: + servers_request = [] + if delete_all: + servers_request = self.get_delete_all_servers_requests() + else: + servers_request = self.get_delete_servers_requests(servers_config) + + if servers_request: + requests.extend(servers_request) + + return requests + + def get_create_servers_requests(self, configs, have): + + requests = [] + + # Create URL and payload + method = PATCH + url = 'data/openconfig-system:system/logging/remote-servers' + server_configs = [] + for config in configs: + req_config = dict() + req_config['host'] = config['host'] + if 'source_interface' in config: + req_config['source-interface'] = config['source_interface'] + if 'message_type' in config: + req_config['message-type'] = config['message_type'] + if 'remote_port' in config: + req_config['remote-port'] = config['remote_port'] + if 'vrf' in config: + req_config['vrf-name'] = config['vrf'] + + server_host = config['host'] + server_config = {"host": server_host, "config": req_config} + server_configs.append(server_config) + + payload = {"openconfig-system:remote-servers": {"remote-server": server_configs}} + request = {"path": url, "method": method, "data": payload} + requests.append(request) + + return requests + + def get_delete_servers_requests(self, configs): + + requests = [] + + # Create URL and payload + method = DELETE + for config in configs: + server_host = config['host'] + url = 'data/openconfig-system:system/logging/remote-servers/remote-server={0}'.format(server_host) + request = {"path": url, "method": method} + requests.append(request) + + return requests + + def get_delete_all_servers_requests(self): + + requests = [] + + # Create URL and payload + method = DELETE + url = 'data/openconfig-system:system/logging/remote-servers' + request = {"path": url, "method": method} + requests.append(request) + + return requests diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mac/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mac/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mac/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mac/mac.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mac/mac.py new file mode 100644 index 000000000..866ff3934 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/mac/mac.py @@ -0,0 +1,431 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_mac 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, + get_replaced_config, + send_requests +) + +NETWORK_INSTANCE_PATH = '/data/openconfig-network-instance:network-instances/network-instance' +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'config': {'vrf_name': ''}}, + {'mac_table_entries': {'mac_address': '', 'vlan_id': ''}} +] + + +class Mac(ConfigBase): + """ + The sonic_mac class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'mac', + ] + + def __init__(self, module): + super(Mac, self).__init__(module) + + def get_mac_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) + mac_facts = facts['ansible_network_resources'].get('mac') + if not mac_facts: + return [] + return mac_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + commands = [] + + existing_mac_facts = self.get_mac_facts() + commands, requests = self.set_config(existing_mac_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_mac_facts = self.get_mac_facts() + + result['before'] = existing_mac_facts + if result['changed']: + result['after'] = changed_mac_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_mac_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_mac_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) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(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 + """ + replaced_config = get_replaced_config(want, have, TEST_KEYS) + + if replaced_config: + self.sort_lists_in_config(replaced_config) + self.sort_lists_in_config(have) + is_delete_all = (replaced_config == have) + requests = self.get_delete_mac_requests(replaced_config, have, is_delete_all) + send_requests(self._module, requests) + + commands = want + else: + commands = diff + + requests = [] + + if commands: + requests = self.get_modify_mac_requests(commands) + + if len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + + if have and have != want: + is_delete_all = True + requests = self.get_delete_mac_requests(have, None, is_delete_all) + send_requests(self._module, requests) + have = [] + + commands = [] + requests = [] + + if not have and want: + commands = want + requests = self.get_modify_mac_requests(commands) + + if len(requests) > 0: + commands = update_states(commands, "overridden") + else: + commands = [] + + return 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_mac_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 + """ + is_delete_all = False + # if want is none, then delete ALL + if not want: + commands = have + is_delete_all = True + else: + commands = want + + commands = self.remove_default_entries(commands) + requests = self.get_delete_mac_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_mac_requests(self, commands): + + requests = [] + + if not commands: + return requests + + for cmd in commands: + vrf_name = cmd.get('vrf_name', None) + mac = cmd.get('mac', {}) + if mac: + aging_time = mac.get('aging_time', None) + dampening_interval = mac.get('dampening_interval', None) + dampening_threshold = mac.get('dampening_threshold', None) + mac_table_entries = mac.get('mac_table_entries', []) + fdb_dict = {} + dampening_cfg_dict = {} + if aging_time: + fdb_dict['config'] = {'mac-aging-time': aging_time} + if dampening_interval: + dampening_cfg_dict['interval'] = dampening_interval + if dampening_threshold: + dampening_cfg_dict['threshold'] = dampening_threshold + if mac_table_entries: + entry_list = [] + entries_dict = {} + mac_table_dict = {} + for entry in mac_table_entries: + entry_dict = {} + entry_cfg_dict = {} + mac_address = entry.get('mac_address', None) + vlan_id = entry.get('vlan_id', None) + interface = entry.get('interface', None) + if mac_address: + entry_dict['mac-address'] = mac_address + entry_cfg_dict['mac-address'] = mac_address + if vlan_id: + entry_dict['vlan'] = vlan_id + entry_cfg_dict['vlan'] = vlan_id + if entry_cfg_dict: + entry_dict['config'] = entry_cfg_dict + if interface: + entry_dict['interface'] = {'interface-ref': {'config': {'interface': interface, 'subinterface': 0}}} + if entry_dict: + entry_list.append(entry_dict) + if entry_list: + entries_dict['entry'] = entry_list + if entries_dict: + mac_table_dict['entries'] = entries_dict + if mac_table_dict: + fdb_dict['mac-table'] = mac_table_dict + if fdb_dict: + url = '%s=%s/fdb' % (NETWORK_INSTANCE_PATH, vrf_name) + payload = {'openconfig-network-instance:fdb': fdb_dict} + requests.append({'path': url, 'method': PATCH, 'data': payload}) + if dampening_cfg_dict: + url = '%s=%s/openconfig-mac-dampening:mac-dampening' % (NETWORK_INSTANCE_PATH, vrf_name) + payload = {'openconfig-mac-dampening:mac-dampening': {'config': dampening_cfg_dict}} + requests.append({'path': url, 'method': PATCH, 'data': payload}) + + return requests + + def get_delete_mac_requests(self, commands, have, is_delete_all): + requests = [] + + for cmd in commands: + vrf_name = cmd.get('vrf_name', None) + if vrf_name and is_delete_all: + requests.extend(self.get_delete_all_mac_requests(vrf_name)) + else: + mac = cmd.get('mac', {}) + if mac: + aging_time = mac.get('aging_time', None) + dampening_interval = mac.get('dampening_interval', None) + dampening_threshold = mac.get('dampening_threshold', None) + mac_table_entries = mac.get('mac_table_entries', []) + + for cfg in have: + cfg_vrf_name = cfg.get('vrf_name', None) + cfg_mac = cfg.get('mac', {}) + if cfg_mac: + cfg_aging_time = cfg_mac.get('aging_time', None) + cfg_dampening_interval = cfg_mac.get('dampening_interval', None) + cfg_dampening_threshold = cfg_mac.get('dampening_threshold', None) + cfg_mac_table_entries = cfg_mac.get('mac_table_entries', []) + + if vrf_name and vrf_name == cfg_vrf_name: + if aging_time and aging_time == cfg_aging_time: + requests.append(self.get_delete_fdb_cfg_attr(vrf_name, 'mac-aging-time')) + if dampening_interval and dampening_interval == cfg_dampening_interval: + requests.append(self.get_delete_mac_dampening_attr(vrf_name, 'interval')) + if dampening_threshold and dampening_threshold == cfg_dampening_threshold: + requests.append(self.get_delete_mac_dampening_attr(vrf_name, 'threshold')) + + if mac_table_entries: + for entry in mac_table_entries: + mac_address = entry.get('mac_address', None) + vlan_id = entry.get('vlan_id', None) + interface = entry.get('interface', None) + + if cfg_mac_table_entries: + for cfg_entry in cfg_mac_table_entries: + cfg_mac_address = cfg_entry.get('mac_address', None) + cfg_vlan_id = cfg_entry.get('vlan_id', None) + cfg_interface = cfg_entry.get('interface', None) + if mac_address and vlan_id and mac_address == cfg_mac_address and vlan_id == cfg_vlan_id: + if interface and interface == cfg_interface: + requests.append(self.get_delete_mac_table_intf(vrf_name, mac_address, vlan_id)) + elif not interface: + requests.append(self.get_delete_mac_table_entry(vrf_name, mac_address, vlan_id)) + return requests + + def get_delete_all_mac_requests(self, vrf_name): + requests = [] + url = '%s=%s/fdb' % (NETWORK_INSTANCE_PATH, vrf_name) + requests.append({'path': url, 'method': DELETE}) + url = '%s=%s/openconfig-mac-dampening:mac-dampening' % (NETWORK_INSTANCE_PATH, vrf_name) + requests.append({'path': url, 'method': DELETE}) + + return requests + + def get_delete_fdb_cfg_attr(self, vrf_name, attr): + url = '%s=%s/fdb/config/%s' % (NETWORK_INSTANCE_PATH, vrf_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mac_dampening_attr(self, vrf_name, attr): + url = '%s=%s/openconfig-mac-dampening:mac-dampening/config/%s' % (NETWORK_INSTANCE_PATH, vrf_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mac_table_entry(self, vrf_name, mac_address, vlan_id): + url = '%s=%s/fdb/mac-table/entries/entry=%s,%s' % (NETWORK_INSTANCE_PATH, vrf_name, mac_address, vlan_id) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mac_table_intf(self, vrf_name, mac_address, vlan_id): + url = '%s=%s/fdb/mac-table/entries/entry=%s,%s/interface' % (NETWORK_INSTANCE_PATH, vrf_name, mac_address, vlan_id) + request = {'path': url, 'method': DELETE} + + return request + + def get_mac_vrf_name(self, vrf_name): + return vrf_name.get('vrf_name') + + def sort_lists_in_config(self, config): + if config: + config.sort(key=self.get_mac_vrf_name) + for cfg in config: + if 'mac' in cfg and cfg['mac'] is not None: + if 'mac_table_entries' in cfg['mac'] and cfg['mac']['mac_table_entries'] is not None: + cfg['mac']['mac_table_entries'].sort(key=lambda x: (x['mac_address'], x['vlan_id'])) + + def remove_default_entries(self, data): + new_data = [] + + if not data: + return new_data + + for conf in data: + new_conf = {} + vrf_name = conf.get('vrf_name', None) + mac = conf.get('mac', None) + if mac: + new_mac = {} + aging_time = mac.get('aging_time', None) + dampening_interval = mac.get('dampening_interval', None) + dampening_threshold = mac.get('dampening_threshold', None) + mac_table_entries = mac.get('mac_table_entries', None) + + if aging_time and aging_time != 600: + new_mac['aging_time'] = aging_time + if dampening_interval and dampening_interval != 5: + new_mac['dampening_interval'] = dampening_interval + if dampening_threshold and dampening_threshold != 5: + new_mac['dampening_threshold'] = dampening_threshold + if mac_table_entries is not None: + new_mac['mac_table_entries'] = mac_table_entries + if new_mac: + new_conf['mac'] = new_mac + new_conf['vrf_name'] = vrf_name + if new_conf: + new_data.append(new_conf) + + return new_data 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 index 88215e8fc..68c99f0ab 100644 --- 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 @@ -14,16 +14,21 @@ created 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.cfg.base import ( ConfigBase, ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_empties, 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_ranges_in_list, get_normalize_interface_name, normalize_interface_name ) @@ -57,6 +62,17 @@ class Mclag(ConfigBase): 'mclag', ] + mclag_simple_attrs = set({ + 'peer_address', + 'source_address', + 'peer_link', + 'system_mac', + 'keepalive', + 'session_timeout', + 'delay_restore', + 'gateway_mac' + }) + def __init__(self, module): super(Mclag, self).__init__(module) @@ -123,6 +139,11 @@ class Mclag(ConfigBase): vlans_list = unique_ip['vlans'] if vlans_list: normalize_interface_name(vlans_list, self._module, 'vlan') + peer_gateway = want.get('peer_gateway', None) + if peer_gateway: + vlans_list = peer_gateway['vlans'] + if vlans_list: + normalize_interface_name(vlans_list, self._module, 'vlan') members = want.get('members', None) if members: portchannels_list = members['portchannels'] @@ -143,11 +164,13 @@ class Mclag(ConfigBase): """ state = self._module.params['state'] if state == 'deleted': - commands = self._state_deleted(want, have) + commands, requests = self._state_deleted(want, have) elif state == 'merged': diff = get_diff(want, have, TEST_KEYS) - commands = self._state_merged(want, have, diff) - return commands + commands, requests = self._state_merged(want, have, diff) + elif state in ('replaced', 'overridden'): + commands, requests = self._state_replaced_overridden(want, have, state) + return commands, requests def _state_merged(self, want, have, diff): """ The command generator when state is merged @@ -159,7 +182,21 @@ class Mclag(ConfigBase): requests = [] commands = [] if diff: - requests = self.get_create_mclag_request(want, diff) + # Obtain diff for VLAN ranges in unique_ip + if 'unique_ip' in diff and diff['unique_ip'] is not None and diff['unique_ip'].get('vlans'): + if 'unique_ip' in have and have['unique_ip'] is not None and have['unique_ip'].get('vlans'): + diff['unique_ip']['vlans'] = self.get_vlan_range_diff(diff['unique_ip']['vlans'], have['unique_ip']['vlans']) + if not diff['unique_ip']['vlans']: + diff.pop('unique_ip') + + # Obtain diff for VLAN ranges in peer_gateway + if 'peer_gateway' in diff and diff['peer_gateway'] is not None and diff['peer_gateway'].get('vlans'): + if 'peer_gateway' in have and have['peer_gateway'] is not None and have['peer_gateway'].get('vlans'): + diff['peer_gateway']['vlans'] = self.get_vlan_range_diff(diff['peer_gateway']['vlans'], have['peer_gateway']['vlans']) + if not diff['peer_gateway']['vlans']: + diff.pop('peer_gateway') + + requests = self.get_create_mclag_requests(want, diff) if len(requests) > 0: commands = update_states(diff, "merged") return commands, requests @@ -175,19 +212,159 @@ class Mclag(ConfigBase): requests = [] if not want: if have: - requests = self.get_delete_all_mclag_domain_request() + requests = self.get_delete_all_mclag_domain_requests(have) if len(requests) > 0: commands = update_states(have, "deleted") else: + del_unique_ip_vlans = [] + del_peer_gateway_vlans = [] + # Create list of VLANs to be deleted based on VLAN ranges in unique_ip + if 'unique_ip' in want and want['unique_ip'] is not None and want['unique_ip'].get('vlans'): + want_unique_ip = want.pop('unique_ip') + if 'unique_ip' in have and have['unique_ip'] is not None and have['unique_ip'].get('vlans'): + del_unique_ip_vlans = self.get_vlan_range_common(want_unique_ip['vlans'], have['unique_ip']['vlans']) + + # Create list of VLANs to be deleted based on VLAN ranges in peer_gateway + if 'peer_gateway' in want and want['peer_gateway'] is not None and want['peer_gateway'].get('vlans'): + want_peer_gateway = want.pop('peer_gateway') + if 'peer_gateway' in have and have['peer_gateway'] is not None and have['peer_gateway'].get('vlans'): + del_peer_gateway_vlans = self.get_vlan_range_common(want_peer_gateway['vlans'], have['peer_gateway']['vlans']) + 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 del_unique_ip_vlans: + diff_want['unique_ip'] = {'vlans': del_unique_ip_vlans} + if del_peer_gateway_vlans: + diff_want['peer_gateway'] = {'vlans': del_peer_gateway_vlans} + if diff_want: - requests = self.get_delete_mclag_attribute_request(want, diff_want) + requests = self.get_delete_mclag_attribute_requests(have['domain_id'], diff_want) if len(requests) > 0: commands = update_states(diff_want, "deleted") return commands, requests + def _state_replaced_overridden(self, want, have, state): + """ The command generator when state is replaced/overridden + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + commands = [] + requests = [] + if want and not have: + commands = [update_states(want, state)] + requests = self.get_create_mclag_requests(want, want) + elif not want and have: + commands = [update_states(have, 'deleted')] + requests = self.get_delete_all_mclag_domain_requests(have) + elif want and have: + add_command = {} + del_command = {} + delete_all = False + + # If 'domain_id' is modified, delete all mclag configuration. + if want['domain_id'] != have['domain_id']: + del_command = have + add_command = want + delete_all = True + else: + have = have.copy() + want = want.copy() + delete_all_vlans = { + 'unique_ip': False, + 'peer_gateway': False + } + + # Delete unspecified configurations when: + # 1) state is overridden. + # 2) state is replaced and configuration other than + # unique_ip, peer_gateway or members is specified. + delete_unspecified = True + if state == 'replaced' and not self.mclag_simple_attrs.intersection(remove_empties(want).keys()): + delete_unspecified = False + + # Create lists of VLANs to be deleted and added based on VLAN ranges + for option in ('unique_ip', 'peer_gateway'): + have_cfg = {} + want_cfg = {} + # The options are removed from the dict to avoid + # comparing the VLAN ranges two more times using get_diff + if have.get(option) and have[option].get('vlans'): + have_cfg = have.pop(option) + if want.get(option) and 'vlans' in want[option]: + want_cfg = want.pop(option) + + if want_cfg: + if have_cfg: + # Delete all VLANs if empty 'vlans' list is provided + if not want_cfg['vlans']: + delete_all_vlans[option] = True + del_command[option] = have_cfg + else: + have_vlans = set(self.get_vlan_id_list(have_cfg['vlans'])) + want_vlans = set(self.get_vlan_id_list(want_cfg['vlans'])) + if have_vlans.intersection(want_vlans): + del_command[option] = {'vlans': self.get_vlan_range_list(list(have_vlans - want_vlans))} + if not del_command[option]['vlans']: + del_command.pop(option) + add_command[option] = {'vlans': self.get_vlan_range_list(list(want_vlans - have_vlans))} + if not add_command[option]['vlans']: + add_command.pop(option) + else: + delete_all_vlans[option] = True + del_command[option] = have_cfg + add_command[option] = want_cfg + else: + if want_cfg['vlans']: + add_command[option] = want_cfg + else: + if have_cfg and delete_unspecified: + delete_all_vlans[option] = True + del_command[option] = have_cfg + + del_diff = get_diff(self.remove_default_entries(have), want, TEST_KEYS) + for option in del_diff: + if not want.get(option): + if delete_unspecified: + del_command[option] = del_diff[option] + else: + # Delete portchannels that are not specified + if option == 'members' and want.get(option): + del_command[option] = del_diff[option] + + # To update 'gateway_mac' configuration in the device, + # delete already configured value. + if option == 'gateway_mac' and want.get(option): + del_command[option] = del_diff[option] + + diff = get_diff(want, have, TEST_KEYS) + add_command.update(diff) + + if del_command: + del_command['domain_id'] = have['domain_id'] + commands.extend(update_states(del_command, 'deleted')) + if delete_all: + requests = self.get_delete_all_mclag_domain_requests(del_command) + else: + if any(delete_all_vlans.values()): + del_command = deepcopy(del_command) + + # Set 'vlans' to None to delete all VLANs + for option in delete_all_vlans: + if delete_all_vlans[option]: + del_command[option]['vlans'] = None + requests = self.get_delete_mclag_attribute_requests(del_command['domain_id'], del_command) + + if add_command: + add_command['domain_id'] = want['domain_id'] + commands.extend(update_states(add_command, state)) + requests.extend(self.get_create_mclag_requests(add_command, add_command)) + + return commands, requests + def remove_default_entries(self, data): new_data = {} if not data: @@ -196,6 +373,7 @@ class Mclag(ConfigBase): default_val_dict = { 'keepalive': 1, 'session_timeout': 30, + 'delay_restore': 300 } for key, val in data.items(): if not (val is None or (key in default_val_dict and val == default_val_dict[key])): @@ -203,9 +381,9 @@ class Mclag(ConfigBase): return new_data - def get_delete_mclag_attribute_request(self, want, command): + def get_delete_mclag_attribute_requests(self, domain_id, command): requests = [] - url_common = 'data/openconfig-mclag:mclag/mclag-domains/mclag-domain=%s/config' % (want["domain_id"]) + url_common = 'data/openconfig-mclag:mclag/mclag-domains/mclag-domain=%s/config' % (domain_id) method = DELETE if 'source_address' in command and command["source_address"] is not None: url = url_common + '/source-address' @@ -231,16 +409,30 @@ class Mclag(ConfigBase): url = url_common + '/mclag-system-mac' request = {'path': url, 'method': method} requests.append(request) + if 'delay_restore' in command and command['delay_restore'] is not None: + url = url_common + '/delay-restore' + request = {'path': url, 'method': method} + requests.append(request) + if 'peer_gateway' in command and command['peer_gateway'] is not None: + if command['peer_gateway']['vlans'] is None: + request = {'path': 'data/openconfig-mclag:mclag/vlan-ifs/vlan-if', 'method': method} + requests.append(request) + elif command['peer_gateway']['vlans'] is not None: + vlan_id_list = self.get_vlan_id_list(command['peer_gateway']['vlans']) + for vlan in vlan_id_list: + peer_gateway_url = 'data/openconfig-mclag:mclag/vlan-ifs/vlan-if=Vlan{0}'.format(vlan) + request = {'path': peer_gateway_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) + vlan_id_list = self.get_vlan_id_list(command['unique_ip']['vlans']) + for vlan in vlan_id_list: + unique_ip_url = 'data/openconfig-mclag:mclag/vlan-interfaces/vlan-interface=Vlan{0}'.format(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} @@ -251,17 +443,29 @@ class Mclag(ConfigBase): portchannel_url = 'data/openconfig-mclag:mclag/interfaces/interface=%s' % (each['lag']) request = {'path': portchannel_url, 'method': method} requests.append(request) + if 'gateway_mac' in command and command['gateway_mac'] is not None: + request = {'path': 'data/openconfig-mclag:mclag/mclag-gateway-macs/mclag-gateway-mac', 'method': method} + requests.append(request) return requests - def get_delete_all_mclag_domain_request(self): + def get_delete_all_mclag_domain_requests(self, have): requests = [] path = 'data/openconfig-mclag:mclag/mclag-domains' method = DELETE + if have.get('peer_gateway'): + request = {'path': 'data/openconfig-mclag:mclag/vlan-ifs/vlan-if', 'method': method} + requests.append(request) + if have.get('unique_ip'): + request = {'path': 'data/openconfig-mclag:mclag/vlan-interfaces/vlan-interface', 'method': method} + requests.append(request) + if have.get('gateway_mac'): + request = {'path': 'data/openconfig-mclag:mclag/mclag-gateway-macs/mclag-gateway-mac', 'method': method} + requests.append(request) request = {'path': path, 'method': method} requests.append(request) return requests - def get_create_mclag_request(self, want, commands): + def get_create_mclag_requests(self, want, commands): requests = [] path = 'data/openconfig-mclag:mclag/mclag-domains/mclag-domain' method = PATCH @@ -269,6 +473,17 @@ class Mclag(ConfigBase): if payload: request = {'path': path, 'method': method, 'data': payload} requests.append(request) + if 'gateway_mac' in commands and commands['gateway_mac'] is not None: + gateway_mac_path = 'data/openconfig-mclag:mclag/mclag-gateway-macs/mclag-gateway-mac' + gateway_mac_method = PATCH + gateway_mac_payload = { + 'openconfig-mclag:mclag-gateway-mac': [{ + 'gateway-mac': commands['gateway_mac'], + 'config': {'gateway-mac': commands['gateway_mac']} + }] + } + request = {'path': gateway_mac_path, 'method': gateway_mac_method, 'data': gateway_mac_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' @@ -276,6 +491,13 @@ class Mclag(ConfigBase): 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 'peer_gateway' in commands and commands['peer_gateway'] is not None: + if commands['peer_gateway']['vlans'] and commands['peer_gateway']['vlans'] is not None: + peer_gateway_path = 'data/openconfig-mclag:mclag/vlan-ifs/vlan-if' + peer_gateway_method = PATCH + peer_gateway_payload = self.build_create_peer_gateway_payload(commands['peer_gateway']['vlans']) + request = {'path': peer_gateway_path, 'method': peer_gateway_method, 'data': peer_gateway_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' @@ -299,6 +521,8 @@ class Mclag(ConfigBase): 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']) + if 'delay_restore' in commands and commands['delay_restore'] is not None: + temp['delay-restore'] = commands['delay_restore'] mclag_dict = {} if temp: domain_id = {"domain-id": want["domain_id"]} @@ -312,8 +536,18 @@ class Mclag(ConfigBase): 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"}}) + vlan_id_list = self.get_vlan_id_list(commands) + for vlan in vlan_id_list: + vlan_name = 'Vlan{0}'.format(vlan) + payload['openconfig-mclag:vlan-interface'].append({"name": vlan_name, "config": {"name": vlan_name, "unique-ip-enable": "ENABLE"}}) + return payload + + def build_create_peer_gateway_payload(self, commands): + payload = {"openconfig-mclag:vlan-if": []} + vlan_id_list = self.get_vlan_id_list(commands) + for vlan in vlan_id_list: + vlan_name = 'Vlan{0}'.format(vlan) + payload['openconfig-mclag:vlan-if'].append({"name": vlan_name, "config": {"name": vlan_name, "peer-gateway-enable": "ENABLE"}}) return payload def build_create_portchannel_payload(self, want, commands): @@ -321,3 +555,63 @@ class Mclag(ConfigBase): for each in commands: payload['openconfig-mclag:interface'].append({"name": each['lag'], "config": {"name": each['lag'], "mclag-domain-id": want['domain_id']}}) return payload + + def get_vlan_range_common(self, config_vlans, match_vlans): + """Returns the vlan ranges present in both 'config_vlans' + and 'match_vlans' in vlans spec format + """ + if not config_vlans: + return [] + + if not match_vlans: + return [] + + config_vlans = self.get_vlan_id_list(config_vlans) + match_vlans = self.get_vlan_id_list(match_vlans) + return self.get_vlan_range_list(list(set(config_vlans).intersection(set(match_vlans)))) + + def get_vlan_range_diff(self, config_vlans, match_vlans): + """Returns the vlan ranges present only in 'config_vlans' + and not in 'match_vlans' in vlans spec format + """ + if not config_vlans: + return [] + + if not match_vlans: + return config_vlans + + config_vlans = self.get_vlan_id_list(config_vlans) + match_vlans = self.get_vlan_id_list(match_vlans) + return self.get_vlan_range_list(list(set(config_vlans) - set(match_vlans))) + + @staticmethod + def get_vlan_id_list(vlan_range_list): + """Returns a list of all VLAN IDs specified in VLAN range list""" + vlan_id_list = [] + if vlan_range_list: + for vlan_range in vlan_range_list: + vlan_val = vlan_range['vlan'] + if '-' in vlan_val: + match = re.match(r'Vlan(\d+)-(\d+)', vlan_val) + if match: + vlan_id_list.extend(range(int(match.group(1)), int(match.group(2)) + 1)) + else: + # Single VLAN ID + match = re.match(r'Vlan(\d+)', vlan_val) + if match: + vlan_id_list.append(int(match.group(1))) + + return vlan_id_list + + @staticmethod + def get_vlan_range_list(vlan_id_list): + """Returns a list of VLAN ranges for given list of VLAN IDs + in vlans spec format""" + vlan_range_list = [] + + if vlan_id_list: + vlan_id_list.sort() + for vlan_range in get_ranges_in_list(vlan_id_list): + vlan_range_list.append({'vlan': 'Vlan{0}'.format('-'.join(map(str, (vlan_range[0], vlan_range[-1])[:len(vlan_range)])))}) + + return vlan_range_list 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 index a4fdc7e0a..b48fa54f6 100644 --- 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 @@ -14,8 +14,6 @@ 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, ) @@ -29,20 +27,30 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s ) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( get_diff, + get_replaced_config, update_states, - normalize_interface_name, normalize_interface_name_list ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + __DELETE_LEAFS_OR_CONFIG_IF_NO_NON_KEY_LEAF, + get_new_config, + get_formatted_config_diff +) + 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": ""} - } + {"servers": {"address": ""}}, + {"ntp_keys": {"key_id": ""}} +] +TEST_KEYS_formatted_diff = [ + {'__default_ops': {'__delete_op': __DELETE_LEAFS_OR_CONFIG_IF_NO_NON_KEY_LEAF}}, + {"servers": {"address": "", '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {"ntp_keys": {"key_id": "", '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}} ] @@ -106,6 +114,17 @@ class Ntp(ConfigBase): if result['changed']: result['after'] = changed_ntp_facts + new_config = changed_ntp_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_ntp_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_ntp_facts, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -119,7 +138,7 @@ class Ntp(ConfigBase): """ want = self._module.params['config'] if want is None: - want = [] + want = {} have = existing_ntp_facts @@ -145,6 +164,10 @@ class Ntp(ConfigBase): commands, requests = self._state_deleted(want, have) elif state == 'merged': commands, requests = self._state_merged(want, have) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have) return commands, requests @@ -160,6 +183,8 @@ class Ntp(ConfigBase): commands = diff requests = [] + + self.preprocess_merge_commands(commands, want) if commands: requests = self.get_merge_requests(commands, have) @@ -207,6 +232,77 @@ class Ntp(ConfigBase): return commands, requests + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + replaced_config = get_replaced_config(want, have, TEST_KEYS) + + add_commands = [] + if replaced_config: + self.sort_lists_in_config(replaced_config) + self.sort_lists_in_config(have) + delete_all = (replaced_config == have) + del_requests = self.get_delete_requests(replaced_config, delete_all) + requests.extend(del_requests) + commands.extend(update_states(replaced_config, "deleted")) + + add_commands = want + else: + diff = get_diff(want, have, TEST_KEYS) + add_commands = diff + + if add_commands: + self.preprocess_merge_commands(add_commands, want) + add_requests = self.get_merge_requests(add_commands, have) + + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "replaced")) + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + + commands = [] + requests = [] + + if have and have != want: + delete_all = True + del_requests = self.get_delete_requests(have, delete_all) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + add_commands = want + add_requests = self.get_merge_requests(add_commands, have) + + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "overridden")) + + return commands, requests + def validate_want(self, want, state): if state == 'deleted': @@ -215,7 +311,9 @@ class Ntp(ConfigBase): 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: + prefer_config = server.get('prefer', None) + if key_id_config or minpoll_config or maxpoll_config or \ + prefer_config is not None: err_msg = "NTP server parameter(s) can not be deleted." self._module.fail_json(msg=err_msg, code=405) @@ -247,6 +345,52 @@ class Ntp(ConfigBase): server.pop('minpoll') if 'maxpoll' in server and not server['maxpoll']: server.pop('maxpoll') + if 'prefer' in server and server['prefer'] is None: + server.pop('prefer') + + if state == 'replaced' or state == 'overridden': + enable_auth_want = want.get('enable_ntp_auth', None) + if enable_auth_want is None: + want['enable_ntp_auth'] = False + if 'servers' in want and want['servers'] is not None: + for server in want['servers']: + if 'prefer' in server and server['prefer'] is None: + server['prefer'] = False + + def search_servers(self, svr_address, servers): + + found_server = dict() + if servers is not None: + for server in servers: + if server['address'] == svr_address: + found_server = server + return found_server + + def preprocess_merge_commands(self, commands, want): + + if 'servers' in commands and commands['servers'] is not None: + for server in commands['servers']: + if 'minpoll' in server and 'maxpoll' not in server: + want_server = dict() + if 'servers' in want: + want_server = self.search_servers(server['address'], want['servers']) + + if want_server: + server['maxpoll'] = want_server['maxpoll'] + else: + err_msg = "Internal error with NTP server maxpoll configuration." + self._module.fail_json(msg=err_msg, code=500) + + if 'maxpoll' in server and 'minpoll' not in server: + want_server = dict() + if 'servers' in want: + want_server = self.search_servers(server['address'], want['servers']) + + if want_server: + server['minpoll'] = want_server['minpoll'] + else: + err_msg = "Internal error with NTP server minpoll configuration." + self._module.fail_json(msg=err_msg, code=500) def get_merge_requests(self, configs, have): @@ -448,18 +592,23 @@ class Ntp(ConfigBase): # 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 \ + if 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) + servers_config = configs.get('servers', None) + if servers_config: + url = 'data/openconfig-system:system/ntp/servers' + 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' @@ -546,3 +695,20 @@ class Ntp(ConfigBase): requests.append(request) return requests + + def get_server_address(self, ntp_server): + return ntp_server.get('address') + + def get_ntp_key_id(self, ntp_key): + return ntp_key.get('key_id') + + def sort_lists_in_config(self, config): + + if 'source_interfaces' in config and config['source_interfaces'] is not None: + config['source_interfaces'].sort() + if 'servers' in config and config['servers'] is not None: + config['servers'].sort(key=self.get_server_address) + if 'trusted_keys' in config and config['trusted_keys'] is not None: + config['trusted_keys'].sort() + if 'ntp_keys' in config and config['ntp_keys'] is not None: + config['ntp_keys'].sort(key=self.get_ntp_key_id) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/pki/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/pki/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/pki/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/pki/pki.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/pki/pki.py new file mode 100644 index 000000000..163e59023 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/pki/pki.py @@ -0,0 +1,563 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 Dell EMC +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_pki 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, + 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 ( + update_states, + get_diff, +) + +from urllib.parse import quote + + +TRUST_STORES_PATH = "data/openconfig-pki:pki/trust-stores" +SECURITY_PROFILES_PATH = "data/openconfig-pki:pki/security-profiles" +TRUST_STORE_PATH = "data/openconfig-pki:pki/trust-stores/trust-store" +SECURITY_PROFILE_PATH = ( + "data/openconfig-pki:pki/security-profiles/security-profile" +) + +PATCH = "patch" +DELETE = "delete" +PUT = "put" +TEST_KEYS = [ + {"security_profiles": {"profile_name": ""}}, + {"trust_stores": {"name": ""}}, +] + + +class Pki(ConfigBase): + """ + The sonic_pki class + """ + + gather_subset = [ + "!all", + "!min", + ] + + gather_network_resources = [ + "pki", + ] + + def get_pki_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 + ) + pki_facts = facts["ansible_network_resources"].get("pki") + if not pki_facts: + return {} + return pki_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_pki_facts = self.get_pki_facts() + commands, requests = self.set_config(existing_pki_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_pki_facts = self.get_pki_facts() + + result["before"] = existing_pki_facts + if result["changed"]: + result["after"] = changed_pki_facts + + result["warnings"] = warnings + + return result + + def set_config(self, existing_pki_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_pki_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, list(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) + return commands, requests + + def _state_replaced(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 + """ + spdiff = sp_diff(want, have) + tsdiff = ts_diff(want, have) + commands = [] + requests = [] + have_dict = { + "security_profiles": { + sp.get("profile_name"): sp + for sp in (have.get("security_profiles") or []) + }, + "trust_stores": { + ts.get("name"): ts for ts in (have.get("trust_stores") or []) + }, + } + for ts in tsdiff: + requests.append( + { + "path": TRUST_STORE_PATH + "=" + ts.get("name"), + "method": PUT, + "data": mk_ts_config(ts), + } + ) + commands.append( + update_states( + have_dict["trust_stores"][ts.get("name")], "replaced" + ) + ) + for sp in spdiff: + requests.append( + { + "path": SECURITY_PROFILE_PATH + + "=" + + sp.get("profile_name"), + "method": PUT, + "data": mk_sp_config(sp), + } + ) + commands.append( + update_states( + have_dict["security_profiles"][sp.get("profile_name")], + "replaced", + ) + ) + + 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 = [] + want_tss = [ts.get("name") for ts in (want.get("trust_stores") or [])] + want_sps = [ + sp.get("profile_name") + for sp in (want.get("security_profiles") or []) + ] + have_tss = [ts.get("name") for ts in (have.get("trust_stores") or [])] + have_sps = [ + sp.get("profile_name") + for sp in (have.get("security_profiles") or []) + ] + + have_dict = { + "security_profiles": { + sp.get("profile_name"): sp + for sp in (have.get("security_profiles") or []) + }, + "trust_stores": { + ts.get("name"): ts for ts in (have.get("trust_stores") or []) + }, + } + used_ts = [] + for sp in have_sps: + if sp not in want_sps: + requests.append( + { + "path": SECURITY_PROFILE_PATH + "=" + sp, + "method": DELETE, + } + ) + commands.append( + update_states( + have_dict["security_profiles"][sp], "deleted" + ) + ) + else: + ts_name = have_dict.get("security_profiles", {}).get(sp, {}).get("trust_store") + if ts_name and ts_name not in used_ts: + used_ts.append(ts_name) + + for ts in have_tss: + if ts not in want_tss and ts not in used_ts: + requests.append( + {"path": TRUST_STORE_PATH + "=" + ts, "method": DELETE} + ) + commands.append( + update_states(have_dict["trust_stores"][ts], "deleted") + ) + + for ts in want.get("trust_stores") or []: + if ts != have_dict["trust_stores"].get(ts.get("name")): + requests.append( + { + "path": TRUST_STORE_PATH + "=" + ts.get("name"), + "method": PUT, + "data": mk_ts_config(ts), + } + ) + commands.append(update_states(ts, "overridden")) + for sp in want.get("security_profiles") or []: + if sp != have_dict["security_profiles"].get( + sp.get("profile_name") + ): + requests.append( + { + "path": SECURITY_PROFILE_PATH + + "=" + + sp.get("profile_name"), + "method": PUT, + "data": mk_sp_config(sp), + } + ) + commands.append(update_states(sp, "overridden")) + + 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 or {} + requests = [] + + for ts in commands.get("trust_stores") or []: + requests.append( + { + "path": TRUST_STORE_PATH, + "method": PATCH, + "data": mk_ts_config(ts), + } + ) + + for sp in commands.get("security_profiles") or []: + requests.append( + { + "path": SECURITY_PROFILE_PATH, + "method": PATCH, + "data": mk_sp_config(sp), + } + ) + + if commands and requests: + 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 = [] + requests = [] + current_ts = [ + ts.get("name") + for ts in (have.get("trust_stores") or []) + if ts.get("name") + ] + current_sp = [ + sp.get("profile_name") + for sp in (have.get("security_profiles") or []) + if sp.get("profile_name") + ] + if not want: + commands = have + for sp in current_sp: + requests.append( + { + "path": SECURITY_PROFILE_PATH + "=" + sp, + "method": DELETE, + } + ) + for ts in current_ts: + requests.append( + {"path": TRUST_STORE_PATH + "=" + ts, "method": DELETE} + ) + else: + commands = remove_empties(want) + + for sp in commands.get("security_profiles") or []: + if sp.get("profile_name") in current_sp: + requests.extend(mk_sp_delete(sp, have)) + for ts in commands.get("trust_stores") or []: + if ts.get("name") in current_ts: + requests.extend(mk_ts_delete(ts, have)) + + if commands and requests: + commands = update_states([commands], "deleted") + else: + commands = [] + + return commands, requests + + +def sp_diff(want, have): + hsps = {} + wsps = {} + dsps = [] + for hsp in have.get("security_profiles") or []: + hsps[hsp.get("profile_name")] = hsp + for wsp in want.get("security_profiles") or []: + wsps[wsp.get("profile_name")] = wsp + + for spn, sp in wsps.items(): + dsp = dict(hsps.get(spn)) + # Pop each leaf from dsp that is not in sp + for k, v in dsp.items(): + if not isinstance(dsp.get(k), list) and not isinstance( + dsp.get(k), dict + ): + if k not in sp: + dsp.pop(k) + for k, v in sp.items(): + if not isinstance(dsp.get(k), list) and not isinstance( + dsp.get(k), dict + ): + if dsp.get(k) != v: + dsp[k] = v + else: + if v is not None: + dsp[k] = v + if dsp != hsps.get(spn): + dsps.append(dsp) + return dsps + + +def ts_diff(want, have): + htss = {} + wtss = {} + dtss = [] + for hts in have.get("trust_stores") or []: + htss[hts.get("name")] = hts + for wts in want.get("trust_stores") or []: + wtss[wts.get("name")] = wts + + for tsn, ts in wtss.items(): + dts = dict(htss.get(tsn)) + for k, v in ts.items(): + if not isinstance(dts.get(k), list) and not isinstance( + dts.get(k), dict + ): + if dts.get(k) != v: + dts[k] = v + else: + if v is not None: + dts[k] = v + if dts != htss.get(tsn): + dtss.append(dts) + return dtss + + +def mk_sp_config(indata): + outdata = { + k.replace("_", "-"): v for k, v in indata.items() if v is not None + } + output = { + "openconfig-pki:security-profile": [ + {"profile-name": outdata.get("profile-name"), "config": outdata} + ] + } + return output + + +def mk_ts_config(indata): + outdata = { + k.replace("_", "-"): v for k, v in indata.items() if v is not None + } + output = { + "openconfig-pki:trust-store": [ + {"name": outdata.get("name"), "config": outdata} + ] + } + return output + + +def mk_sp_delete(want_sp, have): + requests = [] + cur_sp = None + del_sp = {} + for csp in have.get("security_profiles") or []: + if csp.get("profile_name") == want_sp.get("profile_name"): + cur_sp = csp + break + if cur_sp: + for k, v in want_sp.items(): + if v is not None and k != "profile_name": + if v == cur_sp.get(k) or isinstance(v, list): + del_sp[k] = v + if len(del_sp) == 0 and len(want_sp) <= 1: + requests = [ + { + "path": SECURITY_PROFILE_PATH + + "=" + + want_sp.get("profile_name"), + "method": DELETE, + } + ] + else: + for k, v in del_sp.items(): + if isinstance(v, list): + for li in v: + if li in (cur_sp.get(k) or []): + requests.append( + { + "path": SECURITY_PROFILE_PATH + + "=" + + want_sp.get("profile_name") + + "/config/" + + k.replace("_", "-") + + "=" + + quote(li, safe=""), + "method": DELETE, + } + ) + else: + requests.append( + { + "path": SECURITY_PROFILE_PATH + + "=" + + want_sp.get("profile_name") + + "/config/" + + k.replace("_", "-"), + "method": DELETE, + } + ) + return requests + + +def mk_ts_delete(want_ts, have): + requests = [] + cur_ts = None + del_ts = {} + for cts in have.get("trust_stores") or []: + if cts.get("name") == want_ts.get("name"): + cur_ts = cts + break + if cur_ts: + for k, v in want_ts.items(): + if v is not None and k != "name": + if v == cur_ts.get(k) or isinstance(v, list): + del_ts[k] = v + if len(del_ts) == 0 and len(want_ts) <= 1: + requests = [ + { + "path": TRUST_STORE_PATH + "=" + want_ts.get("name"), + "method": DELETE, + } + ] + else: + for k, v in del_ts.items(): + if isinstance(v, list): + for li in v: + if li in (cur_ts.get(k) or []): + requests.append( + { + "path": TRUST_STORE_PATH + + "=" + + want_ts.get("name") + + "/config/" + + k.replace("_", "-") + + "=" + + quote(li, safe=""), + "method": DELETE, + } + ) + else: + requests.append( + { + "path": TRUST_STORE_PATH + + "=" + + want_ts.get("name") + + "/config/" + + k.replace("_", "-"), + "method": DELETE, + } + ) + 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 index 371019d04..654e34dee 100644 --- 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 @@ -17,6 +17,7 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.c ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( to_list, + search_obj_in_list ) from ansible.module_utils.connection import ConnectionError from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import Facts @@ -33,7 +34,6 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s PATCH = 'patch' DELETE = 'delete' -POST = 'post' class Port_breakout(ConfigBase): @@ -152,6 +152,52 @@ class Port_breakout(ConfigBase): return commands, requests + def _state_replaced(self, want, have, diff): + """ The command generator when state is replaced + + :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, "replaced") + else: + commands = [] + + return commands, requests + + def _state_overridden(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 = [] + requests = [] + + # Delete port-breakout configuration for interfaces that are not specified + for cfg in have: + if not search_obj_in_list(cfg['name'], want, 'name'): + commands.append(cfg) + requests.append(self.get_delete_single_port_breakout(cfg['name'], cfg)) + + if commands: + commands = update_states(commands, "deleted") + + add_requests = self.get_modify_port_breakout_requests(diff, have) + if len(add_requests) > 0: + commands.extend(update_states(diff, "overridden")) + requests.extend(add_requests) + + return commands, requests + def _state_deleted(self, want, have, diff): """ The command generator when state is deleted @@ -161,7 +207,7 @@ class Port_breakout(ConfigBase): :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 want is none, then delete all the port_breakout except admin if not want: commands = have else: @@ -215,27 +261,6 @@ class Port_breakout(ConfigBase): 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: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/port_group/port_group.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/port_group/port_group.py new file mode 100644 index 000000000..37281403e --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/port_group/port_group.py @@ -0,0 +1,380 @@ +# +# -*- 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_port_group 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 + +""" +The use of natsort causes sanity error due to it is not available in python version currently used. +When natsort becomes available, the code here and below using it will be applied. +from natsort import ( + natsorted, + ns +) +""" +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, + remove_empties_from_list +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) +from ansible.module_utils.connection import ConnectionError + +GET = "get" +PATCH = 'patch' +DELETE = 'delete' +url = 'data/openconfig-port-group:port-groups/port-group' + +TEST_KEYS = [ + { + 'config': {'id': ''} + } +] +TEST_KEYS_formatted_diff = [ + {'config': {'id': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}} +] + + +class Port_group(ConfigBase): + """ + The sonic_port_group class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'port_group', + ] + + pg_default_speeds_ready = False + pg_default_speeds = [] + + def __init__(self, module): + super(Port_group, self).__init__(module) + + if not Port_group.pg_default_speeds_ready: + Port_group.pg_default_speeds = self.get_port_group_default_speed() + Port_group.pg_default_speeds_ready = True + + def get_port_group_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_group_facts = facts['ansible_network_resources'].get('port_group') + if not port_group_facts: + return [] + return port_group_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_port_group_facts = self.get_port_group_facts() + commands, requests = self.set_config(existing_port_group_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_group_facts = self.get_port_group_facts() + + result['before'] = existing_port_group_facts + if result['changed']: + result['after'] = changed_port_group_facts + + new_config = changed_port_group_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_port_group_facts, + TEST_KEYS_formatted_diff) + # See the above comment about natsort module + # new_config = natsorted(new_config, key=lambda x: x['id']) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_port_group_facts, + new_config, + self._module._verbosity) + result['warnings'] = warnings + return result + + def set_config(self, existing_port_group_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_group_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) + + tmp_want = remove_empties_from_list(want) + new_want = self.remove_empty_dict_from_list(tmp_want) + + new_diff = self.remove_empty_dict_from_list(diff) + + if state == 'overridden': + commands, requests = self._state_overridden(new_want, have, new_diff) + elif state == 'deleted': + commands, requests = self._state_deleted(new_want, have, new_diff) + elif state == 'merged': + commands, requests = self._state_merged(new_want, have, new_diff) + elif state == 'replaced': + commands, requests = self._state_replaced(new_want, have, new_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 diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = diff + requests = [] + if commands: + requests = self.build_merge_requests(commands) + + if len(requests) > 0: + commands = update_states(commands, "merged") + 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 have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + new_want = self.patch_want_with_default(want) + commands = get_diff(new_want, have, TEST_KEYS) + requests = [] + if commands: + requests = self.build_merge_requests(commands) + + if len(requests) > 0: + commands = update_states(commands, "merged") + else: + commands = [] + + 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 + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = diff + requests = [] + if commands: + requests = self.build_merge_requests(commands) + + if 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 + :param diff: the difference between want and have + :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 groups + + if not want: + tmp_commands = have + else: + tmp_commands = want + tmp_commands = self.preprocess_delete_commands(tmp_commands, have) + + commands = get_diff(tmp_commands, Port_group.pg_default_speeds, TEST_KEYS) + + requests = [] + if commands: + requests = self.build_delete_requests(commands) + + if len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def search_port_groups(self, id, pgs): + + found_pg = dict() + if pgs is not None: + for pg in pgs: + if pg['id'] == id: + found_pg = pg + return found_pg + + def preprocess_delete_commands(self, commands, have): + new_commands = [] + for cmd in commands: + pg_id = cmd['id'] + pg = self.search_port_groups(pg_id, have) + if pg: + new_cmd = {'id': pg_id, 'speed': pg['speed']} + new_commands.append(new_cmd) + + return new_commands + + def remove_empty_dict_from_list(self, dict_list): + new_dict_list = [] + if dict_list: + for dictt in dict_list: + if dictt: + new_dict_list.append(dictt) + + return new_dict_list + + def build_delete_requests(self, confs): + requests = [] + + for conf in confs: + pg_id = conf['id'] + method = DELETE + pg_url = (url + '=%s/config/speed') % (pg_id) + request = {"path": pg_url, "method": method} + requests.append(request) + + return requests + + def build_merge_requests(self, confs): + requests = [] + pgs = [] + for conf in confs: + pg_id = conf['id'] + if 'speed' in conf: + pg_conf = {'id': pg_id, 'speed': 'openconfig-if-ethernet:' + conf['speed']} + pg = {'id': pg_id, 'config': pg_conf} + pgs.append(pg) + + if pgs: + payload = {"openconfig-port-group:port-group": pgs} + method = PATCH + pg_url = url + request = {"path": pg_url, "method": method, "data": payload} + requests.append(request) + + return requests + + def patch_want_with_default(self, want): + new_want = list() + for dpg in Port_group.pg_default_speeds: + pg_id = dpg['id'] + pg = self.search_port_groups(pg_id, want) + if pg: + new_pg = {'id': pg_id, 'speed': pg['speed']} + else: + new_pg = {'id': pg_id, 'speed': dpg['speed']} + + new_want.append(new_pg) + return new_want + + def get_port_group_default_speed(self): + """Get all the port group default speeds""" + + pgs_request = [{"path": "data/openconfig-port-group:port-groups/port-group", "method": GET}] + try: + pgs_response = edit_config(self._module, to_request(self._module, pgs_request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + pgs_config = [] + if "openconfig-port-group:port-group" in pgs_response[0][1]: + pgs_config = pgs_response[0][1].get("openconfig-port-group:port-group", []) + + pgs_dft_speeds = [] + for pg_config in pgs_config: + pg_state = dict() + if 'state' in pg_config: + pg_state['id'] = pg_config['id'] + dft_speed_str = pg_config['state'].get('default-speed', None) + if dft_speed_str: + pg_state['speed'] = dft_speed_str.split(":", 1)[-1] + pgs_dft_speeds.append(pg_state) + + return pgs_dft_speeds 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 index d5c36d3e2..f4dc71214 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2019 Red Hat +# Copyright 2023 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) """ @@ -40,7 +40,7 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s TEST_KEYS = [ {"config": {"afi": "", "name": ""}}, - {"prefixes": {"action": "", "ge": "", "le": "", "prefix": "", "sequence": ""}} + {"prefixes": {"ge": "", "le": "", "prefix": "", "sequence": ""}} ] DELETE = "delete" @@ -149,6 +149,10 @@ openconfig-routing-policy-ext:extended-prefixes/extended-prefix={},{},{}' commands, requests = self._state_deleted(want, have) elif state == 'merged': commands, requests = self._state_merged(diff) + elif state == 'replaced': + commands, requests = self._state_replaced(diff) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) ret_commands = commands return ret_commands, requests @@ -188,6 +192,51 @@ openconfig-routing-policy-ext:extended-prefixes/extended-prefix={},{},{}' commands = [] return commands, requests + def _state_replaced(self, diff): + """ The command generator when state is replaced + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = diff + requests = self.get_modify_prefix_lists_requests(commands) + if commands and len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + + if have and have != want: + del_requests = self.get_delete_all_prefix_list_cfg_requests() + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + mod_commands = want + mod_requests = self.get_modify_prefix_lists_requests(mod_commands) + + if len(mod_requests) > 0: + requests.extend(mod_requests) + commands.extend(update_states(mod_commands, "overridden")) + + 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, @@ -456,3 +505,13 @@ openconfig-routing-policy-ext:extended-prefixes/extended-prefix={},{},{}' prefix_net['prefixlen'] = int(prefix_val.split("/")[1]) return prefix_net + + def sort_lists_in_config(self, config): + if config: + config.sort(key=self.get_name) + for cfg in config: + if 'prefixes' in cfg and cfg['prefixes']: + cfg['prefixes'].sort(key=lambda x: (x['sequence'], x['action'], x['prefix'])) + + def get_name(self, name): + return name.get('name') 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 index dfa65482f..264ffa014 100644 --- 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 @@ -27,14 +27,25 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( update_states, get_diff, + get_replaced_config, normalize_interface_name, ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + __DELETE_LEAFS_OR_CONFIG_IF_NO_NON_KEY_LEAF, + get_new_config, + get_formatted_config_diff +) PATCH = 'patch' DELETE = 'delete' TEST_KEYS = [ {'host': {'name': ''}}, ] +TEST_KEYS_formatted_diff = [ + {'__default_ops': {'__delete_op': __DELETE_LEAFS_OR_CONFIG_IF_NO_NON_KEY_LEAF}}, + {'host': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] class Radius_server(ConfigBase): @@ -91,6 +102,17 @@ class Radius_server(ConfigBase): if result['changed']: result['after'] = changed_radius_server_facts + new_config = changed_radius_server_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_radius_server_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_radius_server_facts, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -180,6 +202,67 @@ class Radius_server(ConfigBase): 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 diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + replaced_config = get_replaced_config(want, have, TEST_KEYS) + + add_commands = [] + if replaced_config: + del_requests = self.get_delete_radius_server_requests(replaced_config, have) + requests.extend(del_requests) + commands.extend(update_states(replaced_config, "deleted")) + add_commands = want + else: + add_commands = diff + + if add_commands: + add_requests = self.get_modify_radius_server_requests(add_commands, have) + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "replaced")) + + 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 have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + r_diff = get_diff(have, want, TEST_KEYS) + if have and (diff or r_diff): + del_requests = self.get_delete_radius_server_requests(have, have) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + want_commands = want + want_requests = self.get_modify_radius_server_requests(want_commands, have) + + if len(want_requests) > 0: + requests.extend(want_requests) + commands.extend(update_states(want_commands, "overridden")) + + return commands, requests + def get_radius_global_payload(self, conf): payload = {} global_cfg = {} diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/route_maps/route_maps.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/route_maps/route_maps.py new file mode 100644 index 000000000..0b40c30f2 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/route_maps/route_maps.py @@ -0,0 +1,2354 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_route_maps class +The code in this file compares the current configuration (as a dict) +to the configuration provided (as a dict) based on the contents of the +currently executing playbook. The result of the comparison and the end state +requested by the executing playbook are used to to determine the command set +necessary to bring the current configuration to it's desired end-state. +The resulting commands are then transmitted to the target device. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +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, + validate_config +) +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, + get_normalize_interface_name, + check_required + ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + + +TEST_KEYS = [ + {"config": {"map_name": "", "sequence_num": ""}} +] + +DELETE = "delete" +PATCH = "patch" + + +class Route_maps(ConfigBase): + """ + The sonic_route_maps class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'route_maps', + ] + + route_maps_uri = 'data/openconfig-routing-policy:routing-policy/policy-definitions' + route_map_uri = route_maps_uri + '/policy-definition={0}' + route_map_stmt_uri = route_map_uri + '/statements/statement={1}' + route_map_stmt_base_uri = route_map_uri + '/statements/statement={1}/' + route_maps_data_path = 'openconfig-routing-policy:policy-definitions' + + set_community_rest_names = { + 'additive': 'openconfig-routing-policy-ext:ADDITIVE', + 'local_as': 'openconfig-bgp-types:NO_EXPORT_SUBCONFED', + 'no_advertise': 'openconfig-bgp-types:NO_ADVERTISE', + 'no_export': 'openconfig-bgp-types:NO_EXPORT', + 'no_peer': 'openconfig-bgp-types:NOPEER', + 'none': 'openconfig-bgp-types:NONE' + } + + set_extcomm_rest_names = { + 'rt': 'route-target:', + 'soo': 'route-origin:' + } + + def __init__(self, module): + super(Route_maps, self).__init__(module) + + def get_route_maps_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) + route_maps_facts = facts['ansible_network_resources'].get('route_maps') + if not route_maps_facts: + return [] + return route_maps_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_route_maps_facts = self.get_route_maps_facts() + commands, requests = self.set_config(existing_route_maps_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.errno) + result['changed'] = True + result['commands'] = commands + + changed_route_maps_facts = self.get_route_maps_facts() + + result['before'] = existing_route_maps_facts + if result['changed']: + result['after'] = changed_route_maps_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_route_maps_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: + want = self.validate_and_normalize_config(want) + else: + want = [] + + have = existing_route_maps_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 state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(want, have) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have) + return commands, requests + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + # Delete replaced groupings + commands = deepcopy(want) + requests = self.get_delete_replaced_groupings(commands, have) + if not requests: + commands = [] + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + + if requests: + modify_have = [] + else: + modify_have = have + + # Apply the commands from the playbook + diff = get_diff(want, modify_have, TEST_KEYS) + merged_commands = diff + + replaced_requests = self.get_modify_route_maps_requests(merged_commands, want, modify_have) + requests.extend(replaced_requests) + if merged_commands and len(replaced_requests) > 0: + merged_commands = update_states(merged_commands, "replaced") + commands.extend(merged_commands) + + return commands, requests + + def _state_overridden(self, want, have): + """ 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 = [] + if not want: + return commands, requests + + # Determine if there is any configuration specified in the playbook + # that is not contained in the current configuration. + diff_requested = get_diff(want, have, TEST_KEYS) + + # Determine if there is anything already configured that is not + # specified in the playbook. + diff_unwanted = get_diff(have, want, TEST_KEYS) + + # Idempotency check: If the configuration already matches the + # requested configuration with no extra attributes, no + # commands should be executed on the device. + if not diff_requested and not diff_unwanted: + return commands, requests + + # Delete all current route map configuration + commands = have + requests = self.get_delete_all_route_map_cfg_request() + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + + # Apply the commands from the playbook + merged_commands = want + overridden_requests = self.get_modify_route_maps_requests(merged_commands, want, []) + requests.extend(overridden_requests) + if merged_commands and len(overridden_requests) > 0: + merged_commands = update_states(merged_commands, "overridden") + commands.extend(merged_commands) + return commands, requests + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + diff = get_diff(want, have, TEST_KEYS) + commands = diff + requests = self.get_modify_route_maps_requests(commands, want, have) + 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 + """ + requests = [] + if not have or have == []: + commands = [] + elif not want or want == []: + commands = have + requests = self.get_delete_all_route_map_cfg_request() + else: + commands = want + requests = self.get_delete_route_maps_requests(have, commands) + + if commands and len(requests) > 0: + commands = update_states(commands, "deleted") + else: + commands = [] + + return commands, requests + + def get_modify_route_maps_requests(self, commands, want, have): + '''Traverse the input list of configuration "modify" commands + obtained from parsing the input playbook parameters. For each + command, create a route map configuration REST API to modify the route + map specified by the current command.''' + + requests = [] + if not commands: + return requests + + # Create URL and payload + route_maps_payload_list = [] + route_maps_payload_dict = {'policy-definition': route_maps_payload_list} + for command in commands: + if command.get('action') is None: + self.insert_route_map_cmd_action(command, want) + route_map_payload = self.get_modify_single_route_map_request(command, have) + if route_map_payload: + route_maps_payload_list.append(route_map_payload) + + # Note: This is consistent with current CLI behavior, but should be + # revisited if and when the SONiC REST implementation is enhanced + # for the "match peer" attribute. + self.route_map_remove_configured_match_peer(route_map_payload, have, requests) + + route_maps_data = {self.route_maps_data_path: route_maps_payload_dict} + request = {'path': self.route_maps_uri, 'method': PATCH, 'data': route_maps_data} + requests.append(request) + return requests + + def insert_route_map_cmd_action(self, command, want): + '''Insert the "action" value into the specified "command" if it is not + already present. This dictionary member will not be present in the + command obtained from the "diff" utility if it is unchanged from its + currently configured value because it is not a "difference" in the + configuration requested by the playbook versus the current + configuration. It is, however, needed in order to create the + appropriate REST API for modifying other attributes in the route map.''' + + conf_map_name = command.get('map_name', None) + conf_seq_num = command.get('sequence_num', None) + if not conf_map_name or not conf_seq_num: + return + + conf_action = command.get('action', None) + if conf_action: + return + + # Find the corresponding route map statement in the "want" dict + # list and insert it into the current "command" dict. + matching_map_in_want = self.get_matching_map(conf_map_name, conf_seq_num, want) + if matching_map_in_want: + conf_action = matching_map_in_want.get('action') + if conf_action is not None: + command['action'] = conf_action + + def get_modify_single_route_map_request(self, command, have): + '''Create and return the appropriate set of route map REST API attributes + to modify the route map configuration specified by the current "command".''' + + request = {} + if not command: + return request + + conf_map_name = command.get('map_name', None) + conf_action = command.get('action', None) + conf_seq_num = command.get('sequence_num', None) + if not conf_map_name or not conf_action or not conf_seq_num: + return request + + req_seq_num = str(conf_seq_num) + + if conf_action == 'permit': + req_action = 'ACCEPT_ROUTE' + elif conf_action == 'deny': + req_action = 'REJECT_ROUTE' + else: + return request + + # Create a "blank" template for the request + route_map_request = { + 'name': conf_map_name, + 'config': {'name': conf_map_name}, + 'statements': { + 'statement': [ + { + 'name': req_seq_num, + 'config': { + 'name': req_seq_num + }, + 'actions': { + 'config': { + 'policy-result': req_action + } + } + } + ] + } + } + + route_map_statement = route_map_request['statements']['statement'][0] + + self.get_route_map_modify_match_attr(command, route_map_statement) + self.get_route_map_modify_set_attr(command, route_map_statement, have) + self.get_route_map_modify_call_attr(command, route_map_statement) + + return route_map_request + + def get_route_map_modify_match_attr(self, command, route_map_statement): + '''In the dict specified by the input route_map_statement paramenter, + provide REST API definitions of all "match" attributes contained in the + user input command dict specified by the "command" input parameter + to this function.''' + + match_top = command.get('match') + if not match_top: + return + + route_map_statement['conditions'] = {} + + # + # Handle configuration for BGP policy "match" conditions + # ------------------------------------------------------ + route_map_statement['conditions']['openconfig-bgp-policy:bgp-conditions'] = {} + route_map_match_bgp_policy = \ + route_map_statement['conditions']['openconfig-bgp-policy:bgp-conditions'] + + # Handle match as_path + if match_top.get('as_path'): + route_map_match_bgp_policy['match-as-path-set'] = { + 'config': { + 'as-path-set': match_top['as_path'], + 'match-set-options': 'ANY' + } + } + # Handle match evpn + if match_top.get('evpn'): + route_map_match_bgp_policy['openconfig-policy-ext:match-evpn-set'] = \ + {'config': {}} + route_map_match_bgp_evpn = \ + route_map_match_bgp_policy[ + 'openconfig-policy-ext:match-evpn-set']['config'] + if match_top['evpn'].get('default_route') is not None: + boolval = self.yaml_bool_to_python_bool(match_top['evpn']['default_route']) + route_map_match_bgp_evpn['default-type5-route'] = boolval + if match_top['evpn'].get('route_type'): + route_type_rest_name = ('openconfig-bgp-policy-ext:' + + match_top['evpn']['route_type'].upper()) + route_map_match_bgp_evpn['route-type'] = route_type_rest_name + if match_top['evpn'].get('vni'): + route_map_match_bgp_evpn['vni-number'] = match_top['evpn']['vni'] + if not route_map_match_bgp_evpn: + route_map_match_bgp_policy.pop('openconfig-policy-ext:match-evpn-set') + + # Handle BGP policy match configuration under the "config" dictionary + route_map_match_bgp_policy['config'] = {} + if match_top.get('local_preference'): + route_map_match_bgp_policy['config']['local-pref-eq'] = \ + match_top['local_preference'] + if match_top.get('metric'): + route_map_match_bgp_policy['config']['med-eq'] = match_top['metric'] + if match_top.get('origin'): + route_map_match_bgp_policy['config']['origin-eq'] = match_top['origin'].upper() + if match_top.get('community'): + route_map_match_bgp_policy['config']['community-set'] = match_top['community'] + if match_top.get('ext_comm'): + route_map_match_bgp_policy['config']['ext-community-set'] = match_top['ext_comm'] + if match_top.get('ip') and match_top['ip'].get('next_hop'): + route_map_match_bgp_policy[ + 'config']['openconfig-bgp-policy-ext:next-hop-set'] = match_top['ip']['next_hop'] + if not route_map_match_bgp_policy['config']: + route_map_match_bgp_policy.pop('config') + + if not route_map_match_bgp_policy: + route_map_statement['conditions'].pop('openconfig-bgp-policy:bgp-conditions') + + # Handle match interface + if match_top.get('interface'): + route_map_statement['conditions']['match-interface'] = { + 'config': {'interface': match_top['interface']} + } + + # Handle match IP address/prefix + if match_top.get('ip') and match_top['ip'].get('address'): + route_map_statement['conditions']['match-prefix-set'] = { + 'config': { + 'prefix-set': match_top['ip']['address'], + 'match-set-options': 'ANY' + } + } + + # Handle match IPv6 address/prefix + if match_top.get('ipv6') and match_top['ipv6'].get('address'): + if not route_map_statement['conditions'].get('match-prefix-set'): + route_map_statement['conditions']['match-prefix-set'] = { + 'config': { + 'openconfig-routing-policy-ext:ipv6-prefix-set': match_top[ + 'ipv6']['address'], 'match-set-options': 'ANY' + } + } + else: + route_map_statement[ + 'conditions']['match-prefix-set']['config'][ + 'openconfig-routing-policy-ext:ipv6-prefix-set'] = \ + match_top['ipv6']['address'] + + # Handle match peer + if match_top.get('peer'): + peer_list = list(match_top['peer'].values()) + route_map_statement['conditions']['match-neighbor-set'] = { + 'config': { + 'openconfig-routing-policy-ext:address': peer_list + } + } + + # Handle match source protocol + if match_top.get('source_protocol'): + rest_protocol_name = '' + if match_top['source_protocol'] in ('bgp', 'ospf', 'static'): + rest_protocol_name = ('openconfig-policy-types:' + + match_top['source_protocol'].upper()) + elif match_top['source_protocol'] == 'connected': + rest_protocol_name = 'openconfig-policy-types:DIRECTLY_CONNECTED' + + route_map_statement['conditions']['config'] = \ + {'install-protocol-eq': rest_protocol_name} + + # Handle match source VRF + if match_top.get('source_vrf'): + route_map_statement[ + 'conditions'][ + 'openconfig-routing-policy-ext:match-src-network-instance' + ] = {'config': {'name': match_top['source_vrf']}} + + # Handle match tag + if match_top.get('tag'): + route_map_statement['conditions']['match-tag-set'] = { + 'config': { + 'openconfig-routing-policy-ext:tag-value': [match_top['tag']] + } + } + + def get_route_map_modify_set_attr(self, command, route_map_statement, have): + '''In the dict specified by the input route_map_statement paramenter, + provide REST API definitions of all "set" attributes contained in the + user input command dict specified by the "command" input parameter + to this function.''' + + cmd_set_top = command.get('set') + if not cmd_set_top: + return + + # Get the current configuration (if any) for this route map statement + cfg_set_top = {} + conf_map_name = command.get('map_name') + conf_seq_num = command.get('sequence_num') + cmd_rmap_have = self.get_matching_map(conf_map_name, conf_seq_num, have) + if cmd_rmap_have: + cfg_set_top = cmd_rmap_have.get('set') + + route_map_actions = route_map_statement['actions'] + + # Handle configuration for BGP policy "set" conditions + # ---------------------------------------------------- + route_map_actions['openconfig-bgp-policy:bgp-actions'] = {} + route_map_bgp_actions = \ + route_map_actions['openconfig-bgp-policy:bgp-actions'] = {} + # Handle 'set' AS path prepend + if cmd_set_top.get('as_path_prepend'): + route_map_bgp_actions['set-as-path-prepend'] = { + 'config': { + 'openconfig-routing-policy-ext:asn-list': cmd_set_top['as_path_prepend'] + } + } + + # Handle'set' community list delete + if cmd_set_top.get('comm_list_delete'): + route_map_bgp_actions['set-community-delete'] = { + 'config': { + 'community-set-delete': cmd_set_top['comm_list_delete'] + } + } + + # Handle 'set' community + if cmd_set_top.get('community'): + route_map_bgp_actions['set-community'] = { + 'config': { + 'method': 'INLINE', + 'options': 'ADD' + }, + 'inline': { + 'config': { + 'communities': [] + } + } + } + + rmap_set_communities_cfg = \ + route_map_bgp_actions['set-community']['inline']['config']['communities'] + + if cmd_set_top['community'].get('community_number'): + + # Abort the playbook if the Community "none' attribute is configured. + if cfg_set_top: + if (cfg_set_top.get('community') and + cfg_set_top['community'].get('community_attributes') and + 'none' in cfg_set_top['community']['community_attributes']): + self._module.fail_json( + msg='\nPlaybook aborted: The route map "set" community ' + '"none" attribute is configured.\n\nPlease remove ' + 'the conflicting configuration to configure other ' + 'community "set" attributes.\n') + + comm_num_list = cmd_set_top['community']['community_number'] + + for comm_num in comm_num_list: + rmap_set_communities_cfg.append(comm_num) + + if cmd_set_top['community'].get('community_attributes'): + comm_attr_list = [] + comm_attr_list = cmd_set_top['community']['community_attributes'] + if 'none' in comm_attr_list: + # Verify that no other community attributes are being requested + # at the same time as the "none" attribute and that no + # community attributes are currently configured. Abort the + # playbook execution if these conditions are not met. + if len(comm_attr_list) > 1 or rmap_set_communities_cfg: + self._module.fail_json( + msg='\nPlaybook aborted: The route map "set" community "none"' + 'attribute cannot be configured when other "set" community ' + 'attributes are requested or configured.\n\n' + 'Please revise the playbook to configure the "none"' + 'attribute.\n') + + # Abort the playbook if other Community "set" attributes are + # currently configured. + if cfg_set_top: + if (cfg_set_top.get('community') and + (cfg_set_top['community'].get('community_number') or + (cfg_set_top['community'].get('community_attributes') and + 'none' not in cfg_set_top['community']['community_attributes']))): + self._module.fail_json( + msg='\nPlaybook aborted: The route map "set" community "none" ' + ' attribute cannot be configured when other"set" community ' + 'attributes are requested or configured.\n\n' + 'Please remove the conflicting configuration to ' + 'configure the "none" attribue.\n') + + # Proceed with configuring 'none' if the validity checks passed. + rmap_set_communities_cfg.append('openconfig-bgp-types:NONE') + else: + + # Abort the playbook if the Community "none' attribute is configured. + if cfg_set_top: + if (cfg_set_top.get('community') and + cfg_set_top['community'].get('community_attributes') and + 'none' in cfg_set_top['community']['community_attributes']): + self._module.fail_json( + msg='\nPlaybook aborted: The route map "set"community "none" attribute is ' + 'configured.\n\n' + 'Please remove the conflicting configuration to configure ' + 'other community "set" attributes.\n') + + comm_attr_rest_name = { + 'local_as': 'openconfig-bgp-types:NO_EXPORT_SUBCONFED', + 'no_advertise': 'openconfig-bgp-types:NO_ADVERTISE', + 'no_export': 'openconfig-bgp-types:NO_EXPORT', + 'no_peer': 'openconfig-bgp-types:NOPEER', + 'additive': 'openconfig-routing-policy-ext:ADDITIVE' + } + + for comm_attr in comm_attr_list: + rmap_set_communities_cfg.append(comm_attr_rest_name[comm_attr]) + + # Handle set extcommunity + if cmd_set_top.get('extcommunity'): + route_map_bgp_actions['set-ext-community'] = { + 'config': { + 'method': 'INLINE', + 'options': 'ADD' + }, + 'inline': { + 'config': { + 'communities': [] + } + } + } + + rmap_set_extcommunities_cfg = \ + route_map_bgp_actions['set-ext-community']['inline']['config']['communities'] + + if cmd_set_top['extcommunity'].get('rt'): + rt_list = cmd_set_top['extcommunity']['rt'] + + for rt_val in rt_list: + rmap_set_extcommunities_cfg.append("route-target:" + rt_val) + + if cmd_set_top['extcommunity'].get('soo'): + soo_list = cmd_set_top['extcommunity']['soo'] + + for soo in soo_list: + rmap_set_extcommunities_cfg.append("route-origin:" + soo) + + # + # Handle configuration for BGP policy "set" conditions + # to be located within the "config" sub-dictionary + # ---------------------------------------------------- + route_map_bgp_actions['config'] = {} + route_map_bgp_actions_cfg = \ + route_map_actions['openconfig-bgp-policy:bgp-actions']['config'] + + # Handle set IP next hop. + if cmd_set_top.get('ip_next_hop'): + route_map_bgp_actions_cfg['set-next-hop'] = cmd_set_top['ip_next_hop'] + + # Handle set IPv6 next hop. + if cmd_set_top.get('ipv6_next_hop'): + if cmd_set_top['ipv6_next_hop'].get('global_addr'): + route_map_bgp_actions_cfg['set-ipv6-next-hop-global'] = \ + cmd_set_top['ipv6_next_hop']['global_addr'] + if cmd_set_top['ipv6_next_hop'].get('prefer_global') is not None: + boolval = \ + self.yaml_bool_to_python_bool(cmd_set_top['ipv6_next_hop']['prefer_global']) + route_map_bgp_actions_cfg['set-ipv6-next-hop-prefer-global'] = boolval + + # Handle set local preference. + if cmd_set_top.get('local_preference'): + route_map_bgp_actions_cfg['set-local-pref'] = cmd_set_top['local_preference'] + + # Handle set metric + if cmd_set_top.get('metric'): + route_map_actions['metric-action'] = {'config': {}} + route_map_metric_actions = route_map_actions['metric-action']['config'] + + if cmd_set_top['metric'].get('value'): + route_map_metric_actions['metric'] = cmd_set_top['metric']['value'] + route_map_metric_actions['action'] = \ + 'openconfig-routing-policy:METRIC_SET_VALUE' + route_map_bgp_actions_cfg['set-med'] = cmd_set_top['metric']['value'] + elif cmd_set_top['metric'].get('rtt_action'): + if cmd_set_top['metric']['rtt_action'] == 'set': + route_map_metric_actions['action'] = \ + 'openconfig-routing-policy:METRIC_SET_RTT' + elif cmd_set_top['metric']['rtt_action'] == 'add': + route_map_metric_actions['action'] = \ + 'openconfig-routing-policy:METRIC_ADD_RTT' + elif cmd_set_top['metric']['rtt_action'] == 'subtract': + route_map_metric_actions['action'] = \ + 'openconfig-routing-policy:METRIC_SUBTRACT_RTT' + + if not route_map_metric_actions: + route_map_actions.pop('metric-action') + + # Handle set origin + if cmd_set_top.get('origin'): + route_map_bgp_actions_cfg['set-route-origin'] = cmd_set_top['origin'].upper() + + # Handle set weight + if cmd_set_top.get('weight'): + route_map_bgp_actions_cfg['set-weight'] = cmd_set_top['weight'] + + @staticmethod + def get_route_map_modify_call_attr(command, route_map_statement): + '''In the dict specified by the input route_map_statement paramenter, + provide REST API definitions of the "call" attribute (if present) + contained in the user input command dict specified by the "command" + input parameter to this function.''' + + call_val = command.get('call') + if not call_val: + return + + if not route_map_statement.get('conditions'): + route_map_statement['conditions'] = {'config': {}} + elif not route_map_statement['conditions'].get('config'): + route_map_statement['conditions']['config'] = {} + route_map_statement['conditions']['config']['call-policy'] = call_val + + def get_delete_all_route_map_cfg_request(self): + '''Append to the input list of REST API requests the REST API to + Delete all route map configuration''' + requests = [{'path': self.route_maps_uri, 'method': DELETE}] + return requests + + def get_delete_one_route_map_cfg(self, conf_map_name, requests): + '''Append to the input list of REST API requests the REST API to + delete all configuration for the specified route map.''' + + delete_rmap_path = self.route_map_uri.format(conf_map_name) + request = {'path': delete_rmap_path, 'method': DELETE} + requests.append(request) + + def get_delete_route_map_stmt_cfg(self, command, requests): + '''Append to the input list of REST API requests the REST API to + delete all configuration for the route map "statement" (route + map sub-section) specified by the combination of the route + map name and "statement" sequence number in the input + "command" dict.''' + conf_map_name = command.get('map_name') + conf_seq_num = command.get('sequence_num') + req_seq_num = str(conf_seq_num) + + delete_rmap_stmt_path = self.route_map_stmt_uri.format(conf_map_name, req_seq_num) + request = {'path': delete_rmap_stmt_path, 'method': DELETE} + requests.append(request) + + def get_delete_route_maps_requests(self, have, commands): + '''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 appropriate elements from the route map specified by the current command.''' + + requests = [] + if commands: + for command in commands: + # Create requests for "eligible" attributes within the current route + # map statement. The content of the "command" object, on return from + # execution has only the subset of currently configured attributes + # within the full group of requested attributes for deletion from + # this route map statement. + self.get_delete_single_route_map_requests(have, command, requests) + return requests + + def get_delete_single_route_map_requests(self, have, command, requests): + '''Create and return the appropriate set of route map REST APIs + to delete the eligible requestd attributes from the route map + configuration specified by the current "command".''' + + if not command: + return + + # Validate the current command. + conf_map_name = command.get('map_name', None) + if not conf_map_name: + command = {} + return + conf_seq_num = command.get('sequence_num', None) + if not conf_seq_num: + if self.any_rmap_inst_in_have(conf_map_name, have): + self.get_delete_one_route_map_cfg(conf_map_name, requests) + return + + # Get the current configuration (if any) for this route map statement + cmd_rmap_have = self.get_matching_map(conf_map_name, conf_seq_num, have) + if not cmd_rmap_have: + command = {} + return + + # Check for route map statement deletion before proceeding further. + cmd_match_top = command.get('match') + if cmd_match_top: + cmd_match_top = command['match'] + + cmd_set_top = command.get('set') + if cmd_set_top: + cmd_set_top = command['set'] + + if not cmd_match_top and not cmd_set_top: + self.get_delete_route_map_stmt_cfg(command, requests) + return + + # Proceed with validity checking and execution + conf_action = command.get('action', None) + if not conf_action: + self._module.fail_json( + msg="\nThe 'action' attribute is required, but is absent" + "for route map {0} sequence number {1}\n".format( + conf_map_name, conf_seq_num)) + + if conf_action not in ('permit', 'deny'): + self._module.fail_json( + msg="\nInvalid 'action' attribute value {0} for" + "route map {1} sequence number {2}\n".format( + conf_action, conf_map_name, conf_seq_num)) + command = {} + return + + if cmd_match_top: + self.get_route_map_delete_match_attr(command, cmd_rmap_have, requests) + if cmd_set_top: + self.get_route_map_delete_set_attr(command, cmd_rmap_have, requests) + if command: + self.get_route_map_delete_call_attr(command, cmd_rmap_have, requests) + + return + + @staticmethod + def get_matching_map(conf_map_name, conf_seq_num, input_list): + '''In the input list of command or configuration dicts, find the route map + configuration "statement" (if it exists) for the specified map name + and sequence number.''' + for cfg_route_map in input_list: + if cfg_route_map.get('map_name') and cfg_route_map.get('sequence_num'): + if (cfg_route_map['map_name'] == conf_map_name and + cfg_route_map.get('sequence_num') == conf_seq_num): + return cfg_route_map + + return {} + + @staticmethod + def any_rmap_inst_in_have(conf_map_name, have): + '''In the current configuration on the target device, determine if there + is at least one configuration "statement" for the specified route map name + from the input playbook request.''' + for cfg_route_map in have: + if cfg_route_map.get('map_name'): + if cfg_route_map['map_name'] == conf_map_name: + return True + + return False + + def get_route_map_delete_match_attr(self, command, cmd_rmap_have, requests): + '''Append to the input list of REST API requests the REST APIs needed + for deletion of all eligible "match" attributes contained in the + user input command dict specified by the "command" input parameter + to this function. Modify the contents of the "command" object to + remove any attributes that are not currently configured. These + attributes are not "eligible" for deletion and no REST API "request" + is generated for them.''' + + conf_map_name = command['map_name'] + conf_seq_num = command['sequence_num'] + req_seq_num = str(conf_seq_num) + + match_top = command.get('match') + if not match_top: + return + match_keys = match_top.keys() + + cfg_match_top = cmd_rmap_have.get('match') + if not cfg_match_top: + command.pop('match') + return + cfg_match_keys = cfg_match_top.keys() + + match_both_keys = set(match_keys).intersection(cfg_match_keys) + + # Remove any requested deletion items that aren't configured + match_pop_keys = set(match_keys).difference(match_both_keys) + for key in match_pop_keys: + match_top.pop(key) + if not match_top or not match_both_keys: + command.pop('match') + return + + # Handle configuration for BGP policy "match" conditions + self.get_route_map_delete_match_bgp(command, match_both_keys, cmd_rmap_have, requests) + if not command.get('match'): + if 'match' in command: + command.pop('match') + return + + # Handle generic top level match attributes. + generic_match_rest_attr = { + 'interface': 'match-interface', + 'source_vrf': 'openconfig-routing-policy-ext:match-src-network-instance', + 'tag': 'match-tag-set/config/openconfig-routing-policy-ext:tag-value', + 'source_protocol': 'config/install-protocol-eq' + } + + match_delete_req_base = (self.route_map_stmt_base_uri.format(conf_map_name, req_seq_num) + + 'conditions/') + + for key in generic_match_rest_attr: + if key in match_both_keys and match_top[key] == cfg_match_top[key]: + request_uri = match_delete_req_base + generic_match_rest_attr[key] + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + elif key in match_top: + match_top.pop(key) + if not match_top: + command.pop('match') + return + + # Handle match peer + peer_str = '' + if 'peer' in match_both_keys: + if (match_top['peer'].get('interface') and cfg_match_top['peer'].get('interface') and + match_top['peer']['interface'] == cfg_match_top['peer']['interface']): + peer_str = match_top['peer']['interface'] + elif (match_top['peer'].get('ip') and cfg_match_top['peer'].get('ip') and + match_top['peer']['ip'] == cfg_match_top['peer']['ip']): + peer_str = match_top['peer']['ip'] + elif (match_top['peer'].get('ipv6') and cfg_match_top['peer'].get('ipv6') and + match_top['peer']['ipv6'] == cfg_match_top['peer']['ipv6']): + peer_str = match_top['peer']['ipv6'] + else: + match_top.pop('peer') + if not match_top: + command.pop('match') + return + + if peer_str: + request_uri = (match_delete_req_base + + 'match-neighbor-set/config/' + 'openconfig-routing-policy-ext:address={0}'.format(peer_str)) + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + + elif 'peer' in match_top: + match_top.pop('peer') + if not match_top: + command.pop('match') + return + + # Handle match IP address/prefix + if ('ip' in match_both_keys and match_top['ip'].get('address') and + match_top['ip']['address'] == cfg_match_top['ip'].get('address')): + request_uri = match_delete_req_base + 'match-prefix-set/config/prefix-set' + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + elif 'ip' in match_top: + match_top.pop('ip') + if not match_top: + command.pop('match') + return + + # Handle match IPv6 address/prefix + if ('ipv6' in match_both_keys and match_top['ipv6'].get('address') and + match_top['ipv6']['address'] == cfg_match_top['ipv6'].get('address')): + ipv6_attr_name = \ + 'match-prefix-set/config/openconfig-routing-policy-ext:ipv6-prefix-set' + request_uri = (match_delete_req_base + ipv6_attr_name) + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + elif 'ipv6' in match_top: + match_top.pop('ipv6') + if not match_top: + command.pop('match') + return + + def get_route_map_delete_match_bgp(self, command, match_both_keys, cmd_rmap_have, requests): + '''Append to the input list of REST API requests the REST APIs needed + for deletion of all eligible "match" attributes defined within the + BGP match conditions section of the openconfig routing-policy + definitions for "policy-definitions" (route maps).''' + + conf_map_name = command.get('map_name', None) + conf_seq_num = command.get('sequence_num', None) + req_seq_num = str(conf_seq_num) + match_top = command['match'] + cfg_match_top = cmd_rmap_have.get('match') + route_map_stmt_base_uri_fmt = self.route_map_stmt_base_uri.format(conf_map_name, + req_seq_num) + bgp_match_delete_req_base = (route_map_stmt_base_uri_fmt + + 'conditions/openconfig-bgp-policy:bgp-conditions/') + + # Handle BGP match items within the "config" sub-tree in the openconfig REST API definitons. + self.get_route_map_delete_match_bgp_cfg(command, match_both_keys, cmd_rmap_have, requests) + + # Handle as_path + if 'as_path' in match_both_keys and match_top['as_path'] == cfg_match_top['as_path']: + request_uri = bgp_match_delete_req_base + 'match-as-path-set' + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + elif match_top.get('as_path'): + match_top.pop('as_path') + + # Handle match evpn + if 'evpn' in match_both_keys: + evpn_cfg_delete_base = \ + bgp_match_delete_req_base + 'openconfig-bgp-policy-ext:match-evpn-set/config/' + evpn_attrs = match_top['evpn'] + evpn_match_keys = evpn_attrs.keys() + evpn_rest_attr = { + 'default_route': 'default-type5-route', + 'route_type': 'route-type', + 'vni': 'vni-number' + } + pop_list = [] + for key in evpn_match_keys: + if (key not in cfg_match_top['evpn'] or + evpn_attrs[key] != cfg_match_top['evpn'][key]): + pop_list.append(key) + else: + request_uri = evpn_cfg_delete_base + evpn_rest_attr[key] + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + for key in pop_list: + match_top['evpn'].pop(key) + if not match_top['evpn']: + match_top.pop('evpn') + + def get_route_map_delete_match_bgp_cfg(self, command, match_both_keys, cmd_rmap_have, requests): + '''Append to the input list of REST API requests the REST APIs needed + for deletion of all eligible "match" attributes defined within the + BGP match conditions 'config' section of the openconfig routing-policy + definitions for "policy-definitions" (route maps).''' + + match_top = command['match'] + cfg_match_top = cmd_rmap_have.get('match') + conf_map_name = command['map_name'] + conf_seq_num = command['sequence_num'] + req_seq_num = str(conf_seq_num) + bgp_keys = {'metric', 'origin', 'local_preference', 'community', 'ext_comm', 'ip'} + delete_bgp_keys = bgp_keys.intersection(match_both_keys) + if not delete_bgp_keys: + return + delete_bgp_attrs = [] + bgp_match_delete_req_base = (self.route_map_stmt_base_uri.format(conf_map_name, + req_seq_num) + + 'conditions/openconfig-bgp-policy:bgp-conditions/config/') + + # Check for IP next hop deletion. This is a special case because "next_hop" is + # a level below "ip" in the argspec hierarchy. If 'ip' is the only key in + # delete_bgp_keys, and IP next hop deletion is not required, there is no + # BGP condition match attribute deletion required. + if 'ip' in delete_bgp_keys: + if not match_top['ip'].get('next_hop') or not cfg_match_top['ip'].get('next_hop'): + delete_bgp_keys.remove('ip') + if 'next_hop' in match_top['ip']: + match_top['ip'].pop('next_hop') + if not match_top['ip']: + match_top.pop('ip') + if not match_top: + command.pop('match') + return + + if not delete_bgp_keys: + return + else: + if match_top['ip']['next_hop'] == cfg_match_top['ip']['next_hop']: + request_uri = (bgp_match_delete_req_base + + 'openconfig-bgp-policy-ext:next-hop-set') + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + else: + match_top['ip'].pop('next_hop') + if not match_top['ip']: + match_top.pop('ip') + if not match_top: + command.pop('match') + return + + delete_bgp_keys.remove('ip') + if not delete_bgp_keys: + return + + # Check for deletion of other BGP match attributes. + bgp_rest_attr = { + 'community': 'community-set', + 'ext_comm': 'ext-community-set', + 'local_preference': 'local-pref-eq', + 'metric': 'med-eq', + 'origin': 'origin-eq' + } + for key in delete_bgp_keys: + if match_top[key] == cfg_match_top[key]: + bgp_rest_attr_key = bgp_rest_attr[key] + delete_bgp_attrs.append(bgp_rest_attr_key) + else: + match_top.pop(key) + if not match_top: + command.pop('match') + return + + if not delete_bgp_attrs: + return + + # Create requests for deletion of the eligible BGP match attributes. + for attr in delete_bgp_attrs: + request_uri = bgp_match_delete_req_base + attr + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + + def get_route_map_delete_set_attr(self, command, cmd_rmap_have, requests): + '''Append to the input list of REST API requests the REST APIs needed + for deletion of all eligible "set" attributes contained in the + user input command dict specified by the "command" input parameter + to this function. Modify the contents of the "command" object to + remove any attributes that are not currently configured. These + attributes are not "eligible" for deletion and no REST API "request" + is generated for them.''' + + cmd_set_top = command.get('set') + if not cmd_set_top: + return + set_keys = cmd_set_top.keys() + + cfg_set_top = cmd_rmap_have.get('set') + if not cfg_set_top: + command.pop('set') + return + cfg_set_keys = cfg_set_top.keys() + + set_both_keys = set(set_keys).intersection(cfg_set_keys) + if not set_both_keys: + command.pop('set') + return + + conf_map_name = command['map_name'] + conf_seq_num = command['sequence_num'] + req_seq_num = str(conf_seq_num) + set_delete_base = (self.route_map_stmt_base_uri.format(conf_map_name, + req_seq_num) + 'actions/') + + # Handle configuration for BGP policy "set" conditions + self.get_route_map_delete_set_bgp(command, set_both_keys, cmd_rmap_have, requests) + cmd_set_top = command.get('set') + if not cmd_set_top: + command.pop('set') + return + + # Handle metric "set" attributes. + if 'metric' in set_both_keys: + set_delete_metric_base = set_delete_base + 'metric-action/config' + if cmd_set_top['metric'].get('rtt_action'): + if cmd_set_top['metric']['rtt_action'] == cfg_set_top['metric'].get('rtt_action'): + request_uri = set_delete_metric_base + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + else: + cmd_set_top.pop('metric') + if not cmd_set_top: + command.pop('set') + elif cmd_set_top['metric'].get('value'): + set_delete_bgp_base = set_delete_base + 'openconfig-bgp-policy:bgp-actions/' + if cmd_set_top['metric']['value'] == cfg_set_top['metric'].get('value'): + request = {'path': set_delete_metric_base, 'method': DELETE} + requests.append(request) + request = { + 'path': set_delete_bgp_base + 'config/set-med', + 'method': DELETE + } + requests.append(request) + + else: + cmd_set_top.pop('metric') + if not cmd_set_top: + command.pop('set') + else: + # 'metric' is not in set_both_keys + if cmd_set_top.get('metric'): + cmd_set_top.pop('metric') + if not cmd_set_top: + command.pop('set') + return + + def get_route_map_delete_set_bgp(self, command, set_both_keys, cmd_rmap_have, requests): + '''Append to the input list of REST API requests the REST APIs needed + for deletion of all eligible "set" attributes defined within the + BGP "set" conditions section of the openconfig routing-policy + definitions for "policy-definitions" (route maps).''' + + cmd_set_top = command['set'] + cfg_set_top = cmd_rmap_have.get('set') + conf_map_name = command['map_name'] + conf_seq_num = command['sequence_num'] + req_seq_num = str(conf_seq_num) + bgp_set_delete_req_base = (self.route_map_stmt_base_uri.format(conf_map_name, req_seq_num) + + 'actions/openconfig-bgp-policy:bgp-actions/') + + # Handle BGP "set" items within the "config" sub-tree in the openconfig REST API definitons. + self.get_route_map_delete_set_bgp_cfg(command, set_both_keys, cmd_rmap_have, requests) + + # Handle as_path_prepend + if ('as_path_prepend' in set_both_keys and + cmd_set_top['as_path_prepend'] == cfg_set_top['as_path_prepend']): + request_uri = bgp_set_delete_req_base + 'set-as-path-prepend' + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + else: + if cmd_set_top.get('as_path_prepend'): + cmd_set_top.pop('as_path_prepend') + if not cmd_set_top: + return + + # Handle the "community list delete" (comm_list_delete) attribute + if ('comm_list_delete' in set_both_keys and + cmd_set_top['comm_list_delete'] == cfg_set_top['comm_list_delete']): + request_uri = bgp_set_delete_req_base + 'set-community-delete' + request = {'path': request_uri, 'method': DELETE} + requests.append(request) + else: + if cmd_set_top.get('comm_list_delete'): + cmd_set_top.pop('comm_list_delete') + if not cmd_set_top: + return + + # Handle "set community": Handle named attributes first, then handle community numbers + if 'community' not in set_both_keys: + if cmd_set_top.get('community'): + cmd_set_top.pop('community') + if not cmd_set_top: + return + else: + community_attr_remove_list = [] + set_community_delete_attrs = [] + if cmd_set_top['community'].get('community_attributes'): + if cfg_set_top['community'].get('community_attributes'): + # Append eligible entries to the delete list. Remember which entries + # are ineligible. + for community_attr in cmd_set_top['community']['community_attributes']: + if community_attr in cfg_set_top['community']['community_attributes']: + community_rest_name = self.set_community_rest_names[community_attr] + set_community_delete_attrs.append(community_rest_name) + else: + community_attr_remove_list.append(community_attr) + + # Delete ineligible entries from the command list. + for community_attr in community_attr_remove_list: + cmd_set_top['community']['community_attributes'].remove(community_attr) + if not cmd_set_top['community']['community_attributes']: + cmd_set_top['community'].pop('community_attributes') + else: + # No community attribute entries are configured. Pop the corresponding + # commands from the command list. + cmd_set_top['community'].pop('community_attributes') + + if not cmd_set_top['community']: + cmd_set_top.pop('community') + if not cmd_set_top: + return + + # Handle deletion of "set" community numbers. + if cmd_set_top.get('community') and cmd_set_top['community'].get('community_number'): + community_number_remove_list = [] + if cfg_set_top['community'].get('community_number'): + # Append eligible entries to the delete list. Remember which entries + # are ineligible. + for community_number in cmd_set_top['community']['community_number']: + if community_number in cfg_set_top['community']['community_number']: + set_community_delete_attrs.append(community_number) + else: + community_number_remove_list.append(community_number) + + # Delete ineligible entries from the command list. + for community_number in community_number_remove_list: + cmd_set_top['community']['community_number'].remove(community_number) + if not cmd_set_top['community']['community_number']: + cmd_set_top['community'].pop('community_number') + else: + # If no community number entries are configured, pop the entire + # community number command dict. + cmd_set_top['community'].pop('community_number') + + if not cmd_set_top['community']: + cmd_set_top.pop('community') + if not cmd_set_top: + return + + # Format and enqueue a request to delete eligible community attributes + if set_community_delete_attrs: + bgp_set_delete_community_uri = bgp_set_delete_req_base + 'set-community' + bgp_set_delete_comm_payload = \ + {'openconfig-bgp-policy:set-community': {}} + bgp_set_delete_comm_payload_contents = \ + bgp_set_delete_comm_payload['openconfig-bgp-policy:set-community'] + bgp_set_delete_comm_payload_contents['config'] = \ + {'method': 'INLINE', 'options': 'REMOVE'} + bgp_set_delete_comm_payload_contents['inline'] = \ + {'config': {'communities': set_community_delete_attrs}} + + request = { + 'path': bgp_set_delete_community_uri, + 'method': PATCH, + 'data': bgp_set_delete_comm_payload + } + requests.append(request) + + # Handle set "extended community" deletion + if 'extcommunity' not in set_both_keys: + if cmd_set_top.get('extcommunity'): + cmd_set_top.pop('extcommunity') + if not cmd_set_top: + return + else: + set_extcommunity_delete_attrs = [] + + for extcomm_type in self.set_extcomm_rest_names: + ext_comm_number_remove_list = [] + if cmd_set_top['extcommunity'].get(extcomm_type): + if cfg_set_top['extcommunity'].get(extcomm_type): + # Append eligible entries to the delete list. Remember which entries + # are ineligible. + for extcomm_number in cmd_set_top['extcommunity'][extcomm_type]: + if extcomm_number in cfg_set_top['extcommunity'][extcomm_type]: + set_extcommunity_delete_attrs.append( + self.set_extcomm_rest_names[extcomm_type] + extcomm_number) + else: + ext_comm_number_remove_list.append(extcomm_number) + + # Delete ineligible entries from the command list. + for extcomm_number in ext_comm_number_remove_list: + cmd_set_top['extcommunity'][extcomm_type].remove(extcomm_number) + if not cmd_set_top['extcommunity'][extcomm_type]: + cmd_set_top['extcommunity'].pop(extcomm_type) + else: + # If no extcommunity entries of this type are configured, + # pop the entire extcommunity command sub-dict for this type. + cmd_set_top['extcommunity'].pop(extcomm_type) + + if not cmd_set_top['extcommunity']: + cmd_set_top.pop('extcommunity') + if not cmd_set_top: + return + + # Format and enqueue a request to delete eligible extcommunity attributes + if set_extcommunity_delete_attrs: + bgp_set_delete_extcomm_uri = bgp_set_delete_req_base + 'set-ext-community' + bgp_set_delete_extcomm_payload = \ + {'openconfig-bgp-policy:set-ext-community': {}} + bgp_set_delete_comm_payload_contents = \ + bgp_set_delete_extcomm_payload['openconfig-bgp-policy:set-ext-community'] + bgp_set_delete_comm_payload_contents['config'] = \ + {'method': 'INLINE', 'options': 'REMOVE'} + bgp_set_delete_comm_payload_contents['inline'] = \ + {'config': {'communities': set_extcommunity_delete_attrs}} + + request = { + 'path': bgp_set_delete_extcomm_uri, + 'method': PATCH, + 'data': bgp_set_delete_extcomm_payload + } + requests.append(request) + + def get_route_map_delete_set_bgp_cfg(self, command, set_both_keys, cmd_rmap_have, requests): + '''Append to the input list of REST API requests the REST APIs needed + for deletion of all eligible "set" attributes defined within the + BGP set conditions 'config' section of the openconfig routing-policy + definitions for "policy-definitions" (route maps).''' + + cmd_set_top = command['set'] + + cfg_set_top = cmd_rmap_have.get('set') + conf_map_name = command['map_name'] + conf_seq_num = command['sequence_num'] + req_seq_num = str(conf_seq_num) + bgp_set_delete_req_base = (self.route_map_stmt_base_uri.format(conf_map_name, req_seq_num) + + 'actions/openconfig-bgp-policy:bgp-actions/config/') + # Note: Although 'metric' (REST API 'set-med') is in this REST API configuration + # group, it is handled separately as part of deleting the top level, functionally + # related 'metric-action' attribute. + bgp_cfg_keys = {'ip_next_hop', 'origin', 'local_preference', 'ipv6_next_hop', 'weight'} + delete_bgp_keys = bgp_cfg_keys.intersection(set_both_keys) + if not delete_bgp_keys: + for bgp_key in bgp_cfg_keys: + if bgp_key in cmd_set_top: + cmd_set_top.pop(bgp_key) + return + + delete_bgp_attrs = [] + + # Handle the special case of ipv6_next_hop + if 'ipv6_next_hop' in delete_bgp_keys: + delete_bgp_keys.remove('ipv6_next_hop') + ipv6_next_hop_rest_names = { + 'global_addr': 'set-ipv6-next-hop-global', + 'prefer_global': 'set-ipv6-next-hop-prefer-global' + } + for ipv6_next_hop_key in ipv6_next_hop_rest_names: + if cmd_set_top['ipv6_next_hop'].get(ipv6_next_hop_key) is not None: + if (cmd_set_top['ipv6_next_hop'][ipv6_next_hop_key] == + cfg_set_top['ipv6_next_hop'].get(ipv6_next_hop_key)): + delete_bgp_attrs.append(ipv6_next_hop_rest_names[ipv6_next_hop_key]) + else: + cmd_set_top['ipv6_next_hop'].pop(ipv6_next_hop_key) + if not cmd_set_top['ipv6_next_hop']: + cmd_set_top.pop('ipv6_next_hop') + if not cmd_set_top: + return + + if not delete_bgp_keys and not delete_bgp_attrs: + return + + # Handle other BGP "config" attributes + bgp_cfg_rest_names = { + 'ip_next_hop': 'set-next-hop', + 'local_preference': 'set-local-pref', + 'origin': 'set-route-origin', + 'weight': 'set-weight' + } + + for bgp_cfg_key in bgp_cfg_rest_names: + if bgp_cfg_key in delete_bgp_keys: + if cmd_set_top[bgp_cfg_key] == cfg_set_top[bgp_cfg_key]: + delete_bgp_attrs.append(bgp_cfg_rest_names[bgp_cfg_key]) + else: + cmd_set_top.pop(bgp_cfg_key) + + if not cmd_set_top: + command.pop('set') + return + + for delete_bgp_attr in delete_bgp_attrs: + del_set_bgp_cfg_uri = bgp_set_delete_req_base + delete_bgp_attr + request = {'path': del_set_bgp_cfg_uri, 'method': DELETE} + requests.append(request) + + def get_route_map_delete_call_attr(self, command, cmd_rmap_have, requests): + '''Append to the input list of REST API requests the REST API needed + for deletion of the "call" attribute if this attribute it contained in the + user input command dict specified by the "command" input parameter + to this function and it is currently configured. Modify the contents of + the "command" object to remove the "call" attribute if it is not currently + configured.''' + + if not command.get('call'): + return + + if not command['call'] == cmd_rmap_have.get('call'): + command.pop('call') + return + + conf_map_name = command['map_name'] + req_seq_num = str(command['sequence_num']) + + call_delete_req_uri = \ + (self.route_map_stmt_base_uri.format( + conf_map_name, req_seq_num) + 'conditions/config/call-policy') + request = {'path': call_delete_req_uri, 'method': DELETE} + requests.append(request) + + @staticmethod + def yaml_bool_to_python_bool(yaml_bool): + '''Convert the input YAML bool value to a Python bool value''' + boolval = False + if yaml_bool is None: + boolval = False + elif yaml_bool: + boolval = True + + return boolval + + def route_map_remove_configured_match_peer(self, route_map_payload, have, requests): + '''If a route map "match peer" condition is configured in the route map + statement corresponding to the incoming route map update request + specified by the "route_map_payload" input parameter, equeue a REST API request + to delete it.''' + + if (route_map_payload['statements']['statement'][0].get('conditions') and + route_map_payload['statements']['statement'][0] + ['conditions'].get('match-neighbor-set')): + peer = self.match_peer_configured(route_map_payload, have) + if peer: + request = self.create_match_peer_delete_request(route_map_payload, peer) + if request: + requests.append(request) + + def match_peer_configured(self, route_map_payload, have): + '''Determine if the "match peer ..." condition is already configured for the + route map statement corresponding to the incoming route map update request + specified by the "route_map_payload" input parameter. Return the peer string + if a "match peer" condition is already configured. Otherwise, return an empty + string''' + + if not route_map_payload or not have: + return '' + + conf_map_name = route_map_payload.get('name') + conf_seq_num = (route_map_payload['statements']['statement'][0]['name']) + if not conf_map_name or not conf_seq_num: + return '' + + # Get the current configuration (if any) for this route map statement + cmd_rmap_have = self.get_matching_map(conf_map_name, int(conf_seq_num), have) + if (not cmd_rmap_have or not cmd_rmap_have.get('match') or + not cmd_rmap_have['match'].get('peer')): + return '' + + peer_dict = cmd_rmap_have['match']['peer'] + if peer_dict.get('interface'): + peer_str = peer_dict['interface'] + elif peer_dict.get('ip'): + peer_str = peer_dict['ip'] + elif peer_dict.get('ipv6'): + peer_str = peer_dict['ipv6'] + else: + return '' + + return peer_str + + def create_match_peer_delete_request(self, route_map_payload, peer_str): + '''Create a request to delete the current "match peer" configuration for the + route map statement corresponding to the incoming route map update request + specified by the "route_map_payload," input parameter. Return the created request.''' + + if not route_map_payload: + return {} + + conf_map_name = route_map_payload.get('name') + conf_seq_num = route_map_payload['statements']['statement'][0]['name'] + if not conf_map_name or not conf_seq_num: + return {} + match_delete_req_base = (self.route_map_stmt_base_uri.format(conf_map_name, conf_seq_num) + + 'conditions/') + + request_uri = (match_delete_req_base + + 'match-neighbor-set/config/' + 'openconfig-routing-policy-ext:address={0}'.format(peer_str)) + request = {'path': request_uri, 'method': DELETE} + return request + + def get_delete_replaced_groupings(self, commands, have): + '''For each of the route maps specified in the "commands" input list, + create requests to delete any existing route map configuration + groupings for which modified attribute requests are specified.''' + + requests = [] + for command in commands: + self.get_delete_one_map_replaced_groupings(command, have, requests) + return requests + + def get_delete_one_map_replaced_groupings(self, command, have, requests): + '''For the route map specified by the input "command", create requests + to delete any existing route map configuration groupings for which + modified attribute requests are specified''' + + if not command: + return {} + + conf_map_name = command.get('map_name', None) + conf_seq_num = command.get('sequence_num', None) + if not conf_map_name or not conf_seq_num: + return {} + + # Get the current configuration (if any) for this route map + cmd_rmap_have = self.get_matching_map(conf_map_name, conf_seq_num, have) + + # If there's nothing configured for this route map, there's nothing + # to delete. + if not cmd_rmap_have: + command = {} + return command + + self.get_delete_route_map_replaced_match_groupings(command, cmd_rmap_have, requests) + replaced_set_group_requests = [] + self.get_delete_route_map_replaced_set_groupings(command, cmd_rmap_have, + replaced_set_group_requests) + if replaced_set_group_requests: + requests.extend(replaced_set_group_requests) + + # Note: Because the "call" route map attribute is a "flat" attribute, not + # a dictionary, no "pre-delete" is required for this branch of the route map + # argspec for handling of "replaced" state + + return command + + def get_delete_route_map_replaced_match_groupings(self, command, cmd_rmap_have, requests): + '''For the route map specified by the input "command", create requests + to delete any existing route map "match" configuration groupings for which + modified attribute requests are specified''' + + if not command.get('match'): + return + + conf_map_name = command.get('map_name', None) + conf_seq_num = command.get('sequence_num', None) + req_seq_num = str(conf_seq_num) + + cmd_match_top = command['match'] + cfg_match_top = cmd_rmap_have.get('match') + + # If there are no 'match' attributes configured for this route map, + # there's nothing to delete. + if not cfg_match_top: + command.pop('match') + return + + match_delete_req_base = (self.route_map_stmt_base_uri.format(conf_map_name, req_seq_num) + + 'conditions/') + + # Obtain the set of "match" keys for which changes have been requested and + # the subset of those keys for which configuration currently exists. + cmd_match_keys = cmd_match_top.keys() + cfg_match_keys = cfg_match_top.keys() + + peer_str = '' + if 'peer' in cfg_match_keys: + peer_dict = cfg_match_top['peer'] + # Only one peer key at a time can be configured. + peer_key = list(peer_dict.keys())[0] + peer_str = peer_dict[peer_key] + + bgp_match_delete_req_base = match_delete_req_base + 'openconfig-bgp-policy:bgp-conditions/' + match_top_level_keys = [ + 'as_path', + 'community', + 'ext_comm', + 'interface', + 'ipv6', + 'local_preference', + 'metric', + 'origin', + 'peer', + 'source_protocol', + 'source_vrf', + 'tag' + ] + + match_multi_level_keys = [ + 'evpn', + 'ip', + ] + + match_uri_attr = { + 'as_path': bgp_match_delete_req_base + 'match-as-path-set', + 'community': bgp_match_delete_req_base + 'config/community-set', + 'evpn': bgp_match_delete_req_base + 'openconfig-bgp-policy-ext:match-evpn-set/config/', + 'ext_comm': bgp_match_delete_req_base + 'config/ext-community-set', + 'interface': match_delete_req_base + 'match-interface', + 'ip': { + 'address': match_delete_req_base + 'match-prefix-set/config/prefix-set', + 'next_hop': (bgp_match_delete_req_base + + 'config/openconfig-bgp-policy-ext:next-hop-set') + }, + 'ipv6': (match_delete_req_base + + 'match-prefix-set/config/openconfig-routing-policy-ext:ipv6-prefix-set'), + 'local_preference': bgp_match_delete_req_base + 'config/local-pref-eq', + 'metric': bgp_match_delete_req_base + 'config/med-eq', + 'origin': bgp_match_delete_req_base + 'config/origin-eq', + 'peer': (match_delete_req_base + + 'match-neighbor-set/config/' + 'openconfig-routing-policy-ext:address={0}'.format(peer_str)), + 'source_protocol': match_delete_req_base + 'config/install-protocol-eq', + 'source_vrf': (match_delete_req_base + + 'openconfig-routing-policy-ext:match-src-network-instance'), + 'tag': (match_delete_req_base + + 'match-tag-set/config/openconfig-routing-policy-ext:tag-value') + } + + # Remove all appropriate "match" configuration for this route map if any of the + # following criteria are met: (See the note below regarding what configuration + # is "appropriate"for deletion.) + # + # 1) Any top level attribute is specified with a value different from its current + # configured value. + # 2) Any top level attribute is specified that is not currently configured. + # 3) The set of top level attributes specified does not include all currently + # configured attributes (regardless of whether the specified values for + # these attributes are the same as the ones courrently configured). + # (Note: Although the IPv6 attribute is defined as a nested dictionary + # to allow for future expansion, it is handled here as a top level + # attrbute because it currently has only one member.) + # + # When deletion has been triggered, an attribute is deleted only if it is + # not present at all in the requested configuration. (If it is present in + # the requested configuration, the "merge" phase of the "replaced" state + # operation will modify it as needed, so it doesn't need to be explicitly + # deleted during the "deletion" phase.) + # + cfg_top_level_key_set = set(cfg_match_keys).intersection(set(match_top_level_keys)) + cmd_top_level_key_set = set(cmd_match_keys).intersection(set(match_top_level_keys)) + symmetric_diff_set = cmd_top_level_key_set.symmetric_difference(cfg_top_level_key_set) + intersection_diff_set = cmd_top_level_key_set.intersection(cfg_top_level_key_set) + cmd_delete_dict = {} + if (cmd_top_level_key_set and symmetric_diff_set or + (any(keyname for keyname in intersection_diff_set if + cmd_match_top[keyname] != cfg_match_top[keyname]))): + + # Deletion has been triggered. First, delete all approriate top level + # attributes + self.delete_replaced_dict_config( + cfg_key_set=cfg_top_level_key_set, + cmd_key_set=cmd_top_level_key_set, + cfg_parent_dict=cfg_match_top, + uri_attr=match_uri_attr, + uri_dict_key='cfg_dict_member_key', + deletion_dict=cmd_delete_dict, + requests=requests) + + # Next, delete all appropriate sub dictionary attributes. + match_dict_deletions = {} + for match_key in match_multi_level_keys: + cfg_key_set = {} + cmd_key_set = {} + if match_key in cfg_match_top: + cfg_key_set = set(cfg_match_top[match_key].keys()) + if match_key in cfg_match_top: + cmd_key_set = ([]) + if cmd_match_top.get(match_key): + cmd_key_set = set(cmd_match_top[match_key].keys()) + match_dict_deletions[match_key] = {} + match_dict_deletions_subdict = match_dict_deletions[match_key] + self.delete_replaced_dict_config( + cfg_key_set=cfg_key_set, + cmd_key_set=cmd_key_set, + cfg_parent_dict=cfg_match_top[match_key], + uri_attr=match_uri_attr, + uri_dict_key=match_key, + deletion_dict=match_dict_deletions_subdict, + requests=requests) + + # Update the dict specifying deleted commands + command.pop('match') + if cmd_delete_dict: + command['match'] = cmd_delete_dict + command['match'].update(match_dict_deletions) + return + + # If no top level attribute changes were requested, check for changes in + # dictionaries nested below the top level. + # ----------------------------------------------------------------------- + match_key_deletions = {} + for match_key in match_multi_level_keys: + if match_key in cmd_match_top: + if match_key in cfg_match_top: + cmd_key_set = set((cmd_match_top[match_key].keys())) + cfg_key_set = set(cfg_match_top[match_key].keys()) + symmetric_diff_set = cmd_key_set.symmetric_difference(cfg_key_set) + intersection_diff_set = cmd_key_set.intersection(cfg_key_set) + if (symmetric_diff_set or + (any(keyname for keyname in intersection_diff_set if + cmd_match_top[match_key][keyname] != + cfg_match_top[match_key][keyname]))): + + match_key_deletions[match_key] = {} + match_key_deletions_subdict = match_key_deletions[match_key] + self.delete_replaced_dict_config( + cfg_key_set=cfg_key_set, + cmd_key_set=cmd_key_set, + cfg_parent_dict=cfg_match_top[match_key], + uri_attr=match_uri_attr, + uri_dict_key=match_key, + deletion_dict=match_key_deletions_subdict, + requests=requests) + + command.pop('match') + if match_key_deletions: + command['match'] = match_key_deletions + + @staticmethod + def delete_replaced_dict_config(**in_args): + ''' Create and enqueue deletion requests for the appropriate attributes in the dictionary + specified by "dict_key". Update the input deletion_dict with the deleted attributes. + The input 'inargs' is assumed to contain the following keyword arguments: + + cfg_key_set: The set of currently configured keys for the target dict + + cmd_key_set: The set of currently requested update keys for the target dict + + cfg_parent_dict: The configured dictionary containing the input key set + + uri_attr: a dictionary specifying REST URIs keyed by argspec keys + + uri_dict_key: The key for top level attribue to be used for uri lookup. If set + to the string value 'cfg_dict_member_key', the current value of 'cfg_dict_member_key' + is used. Otherwise, the specified value is used directly. + + deletion_dict: a dictionary containing attributes deleted from the parent dict + + requests: The list of REST API requests for the executing playbook section + ''' + + # Set the default uri_key value. + uri_key = in_args['uri_dict_key'] + + # Iterate through members of the parent dict. + for cfg_dict_member_key in in_args['cfg_key_set'].difference(in_args['cmd_key_set']): + cfg_dict_member_val = in_args['cfg_parent_dict'][cfg_dict_member_key] + if in_args['uri_dict_key'] == 'cfg_dict_member_key': + uri_key = cfg_dict_member_key + uri = in_args['uri_attr'][uri_key] + in_args['deletion_dict'].update( + {cfg_dict_member_key: cfg_dict_member_val}) + if isinstance(uri, dict): + for member_key in uri: + if in_args['cfg_parent_dict'].get(member_key) is not None: + request = {'path': uri[member_key], + 'method': DELETE} + in_args['requests'].append(request) + elif isinstance(uri, list): + for set_uri_item in uri: + request = {'path': set_uri_item, 'method': DELETE} + else: + request = {'path': uri, 'method': DELETE} + in_args['requests'].append(request) + + def get_delete_route_map_replaced_set_groupings(self, command, cmd_rmap_have, + requests): + '''For the route map specified by the input "command", create requests + to delete any existing route map "set" configuration groupings for which + modified attribute requests are specified''' + + if not command.get('set'): + return + + conf_map_name = command.get('map_name', None) + conf_seq_num = command.get('sequence_num', None) + req_seq_num = str(conf_seq_num) + + cmd_set_top = command['set'] + cfg_set_top = cmd_rmap_have.get('set') + + # If there are no 'set' attributes configured for this route map, + # there's nothing to delete. + if not cfg_set_top: + command.pop('set') + return + + set_delete_req_base = (self.route_map_stmt_base_uri.format(conf_map_name, req_seq_num) + + 'actions/') + bgp_set_delete_req_base = set_delete_req_base + 'openconfig-bgp-policy:bgp-actions/' + + # Obtain the set of "set" keys for which changes have been requested and the set + # of keys currently configured. + cmd_set_keys = cmd_set_top.keys() + cfg_set_keys = cfg_set_top.keys() + + metric_uri = '' + if 'metric' in cfg_set_top: + if cfg_set_top['metric'].get('rtt_action'): + metric_uri = set_delete_req_base + 'metric-action/config' + elif cfg_set_top['metric'].get('value'): + metric_uri = [set_delete_req_base + 'metric-action/config', + bgp_set_delete_req_base + 'config/set-med'] + # Top level keys: Note: Although "metric" is defined as a dictionary, it + # is handled as a "top level" attribute because it can contain + # only one configured member (either an rtt_action or a "value"). + set_top_level_keys = [ + 'as_path_prepend', + 'comm_list_delete', + 'ip_next_hop', + 'local_preference', + 'metric', + 'origin', + 'weight', + ] + + set_uri_attr = { + 'as_path_prepend': bgp_set_delete_req_base + 'set-as-path-prepend', + 'comm_list_delete': bgp_set_delete_req_base + 'set-community-delete', + 'community': bgp_set_delete_req_base + 'set-community', + 'extcommunity': bgp_set_delete_req_base + 'set-ext-community', + 'ip_next_hop': bgp_set_delete_req_base + 'config/set-next-hop', + 'ipv6_next_hop': { + 'global_addr': bgp_set_delete_req_base + 'config/set-ipv6-next-hop-global', + 'prefer_global': bgp_set_delete_req_base + 'config/set-ipv6-next-hop-prefer-global' + }, + 'local_preference': bgp_set_delete_req_base + 'config/set-local-pref', + 'metric': metric_uri, + 'origin': bgp_set_delete_req_base + 'config/set-route-origin', + 'weight': bgp_set_delete_req_base + 'config/set-weight' + } + + # Remove all appropriate "set" configuration for this route map if any of the + # following criteria are met: (See the note below regarding what configuration + # is "appropriate"for deletion.) + # + # 1) Any top level attribute is specified with a value different from its current + # configured value. + # 2) Any top level attribute is specified that is not currently configured. + # 3) The set of top level attributes specified does not include all currently + # configured attributes (regardless of whether the specified values for + # these attributes are the same as the ones courrently configured). + # (Note: Although the IPv6 attribute is defined as a nested dictionary + # to allow for future expansion, it is handled here as a top level + # attrbute because it currently has only one member.) + # + # When deletion has been triggered, an attribute is deleted only if it is + # not present at all in the requested configuration. (If it is present in + # the requested configuration, the "merge" phase of the "replaced" state + # operation will modify it as needed, so it doesn't need to be explicitly + # deleted during the "deletion" phase.) + # + # Handle top level attributes first. If top level attribute deletion is + # triggered, proceed with deletion of dictionaries and lists below the + # top level. + cfg_top_level_key_set = set(cfg_set_keys).intersection(set(set_top_level_keys)) + cmd_top_level_key_set = set(cmd_set_keys).intersection(set(set_top_level_keys)) + cmd_nested_level_key_set = set(cmd_set_keys).difference(set_top_level_keys) + symmetric_diff_set = cmd_top_level_key_set.symmetric_difference(cfg_top_level_key_set) + intersection_diff_set = cmd_top_level_key_set.intersection(cfg_top_level_key_set) + cmd_delete_dict = {} + if (cmd_top_level_key_set and symmetric_diff_set or + (any(keyname for keyname in intersection_diff_set if + cmd_set_top[keyname] != cfg_set_top[keyname]))): + # Deletion has been triggered. First, delete all approriate top level + # attributes + self.delete_replaced_dict_config( + cfg_key_set=cfg_top_level_key_set, + cmd_key_set=cmd_top_level_key_set, + cfg_parent_dict=cfg_set_top, + uri_attr=set_uri_attr, + uri_dict_key='cfg_dict_member_key', + deletion_dict=cmd_delete_dict, + requests=requests) + + # Save nested command "set" items and refresh top level command "set" items. + cmd_set_nested = {} + for nested_key in cmd_nested_level_key_set: + if command['set'].get(nested_key) is not None: + cmd_set_nested[nested_key] = command['set'][nested_key] + + command.pop('set') + if cmd_delete_dict: + command['set'] = cmd_delete_dict + if cmd_set_nested: + if not command.get('set'): + command['set'] = {} + command['set'].update(cmd_set_nested) + if not command.get('set'): + command['set'] = {} + cmd_set_top = command['set'] + + # Proceed with deletion of dictionaries and lists below the top level. + # --------------------------------------------------------------------- + + dict_delete_requests = [] + + # Check for deletion of set "community" lists. Delete the items in + # the currently configured list if it exists. As an optimization, + # avoid deleting list items that will be replaced by the received + # command. + + set_community_delete_attrs = [] + if 'community' not in cfg_set_top: + if command['set'].get('community'): + command['set'].pop('community') + if command['set'] is None: + command.pop('set') + return + else: + set_community_number_deletions = [] + if 'community_number' in cfg_set_top['community']: + + # Delete eligible configured community numbers. + cfg_community_number_set = set(cfg_set_top['community']['community_number']) + cmd_community_number_set = ([]) + if cmd_set_top.get('community') and 'community_number' in cmd_set_top['community']: + cmd_community_number_set = set(cmd_set_top['community']['community_number']) + command['set']['community'].pop('community_number') + + for cfg_community_number in cfg_community_number_set.difference(cmd_community_number_set): + set_community_delete_attrs.append(cfg_community_number) + set_community_number_deletions.append(cfg_community_number) + + if set_community_number_deletions: + # Update the list of deleted community numbers in the "command" dict. + if not cmd_set_top.get('community'): + command['set']['community'] = {} + command['set']['community']['community_number'] = set_community_number_deletions + + set_community_attributes_deletions = [] + if 'community_attributes' in cfg_set_top['community']: + + # Delete eligible configured community attributes. + cfg_community_attributes_set = set(cfg_set_top['community']['community_attributes']) + cmd_community_attributes_set = ([]) + if cmd_set_top.get('community') and 'community_attributes' in cmd_set_top['community']: + cmd_community_attributes_set = set(cmd_set_top['community']['community_attributes']) + command['set']['community'].pop('community_attributes') + + for cfg_community_attribute in cfg_community_attributes_set.difference(cmd_community_attributes_set): + set_community_delete_attrs.append(self.set_community_rest_names[cfg_community_attribute]) + set_community_attributes_deletions.append(cfg_community_attribute) + + if set_community_attributes_deletions: + # Update the list of deleted community attributes in the "command" dict. + if not cmd_set_top.get('community'): + command['set']['community'] = {} + command['set']['community']['community_attributes'] = set_community_attributes_deletions + + if command['set'].get('community') is not None and not command['set']['community']: + command['set'].pop('community') + + # Format and enqueue a request to delete eligible community attributes + if set_community_delete_attrs: + bgp_set_delete_community_uri = bgp_set_delete_req_base + 'set-community' + bgp_set_delete_comm_payload = \ + {'openconfig-bgp-policy:set-community': {}} + bgp_set_delete_comm_payload_contents = \ + bgp_set_delete_comm_payload['openconfig-bgp-policy:set-community'] + bgp_set_delete_comm_payload_contents['config'] = \ + {'method': 'INLINE', 'options': 'REMOVE'} + bgp_set_delete_comm_payload_contents['inline'] = \ + {'config': {'communities': set_community_delete_attrs}} + + request = { + 'path': bgp_set_delete_community_uri, + 'method': PATCH, + 'data': bgp_set_delete_comm_payload + } + dict_delete_requests.append(request) + + # Check for deletion of set "extcommunity" lists. Delete the items in + # the currently configured list if it exists. As an optimization, + # avoid deleting list items that will be replaced by the received + # command. + set_extcommunity_delete_attrs = [] + + if 'extcommunity' not in cfg_set_top: + if command['set'].get('extcommunity'): + command['set'].pop('extcommunity') + if command['set'] is None: + command.pop('set') + return + else: + for extcomm_type in self.set_extcomm_rest_names: + set_extcommunity_delete_attrs_type = [] + if extcomm_type in cfg_set_top['extcommunity']: + # Delete eligible configured extcommunity list items for this + # extcommunity list + cfg_extcommunity_list_set = set(cfg_set_top['extcommunity'][extcomm_type]) + cmd_extcommunity_list_set = ([]) + if cmd_set_top.get('extcommunity') and extcomm_type in cmd_set_top['extcommunity']: + cmd_extcommunity_list_set = set(cmd_set_top['extcommunity'][extcomm_type]) + command['set']['extcommunity'].pop(extcomm_type) + for extcomm_number in cfg_extcommunity_list_set.difference(cmd_extcommunity_list_set): + set_extcommunity_delete_attrs.append( + self.set_extcomm_rest_names[extcomm_type] + + extcomm_number) + set_extcommunity_delete_attrs_type.append(extcomm_number) + + if set_extcommunity_delete_attrs_type: + # Update the list of deleted extcommunity list items of this type + # in the "command" dict. + if not cmd_set_top.get('extcommunity'): + command['set']['extcommunity'] = {} + command['set']['extcommunity'][extcomm_type] = set_extcommunity_delete_attrs_type + + if command['set'].get('extcommunity') is not None and not command['set']['extcommunity']: + command['set'].pop('extcommunity') + + # Format and enqueue a request to delete eligible extcommunity attributes + if set_extcommunity_delete_attrs: + bgp_set_delete_extcomm_uri = bgp_set_delete_req_base + 'set-ext-community' + bgp_set_delete_extcomm_payload = \ + {'openconfig-bgp-policy:set-ext-community': {}} + bgp_set_delete_comm_payload_contents = \ + bgp_set_delete_extcomm_payload[ + 'openconfig-bgp-policy:set-ext-community'] + bgp_set_delete_comm_payload_contents['config'] = \ + {'method': 'INLINE', 'options': 'REMOVE'} + bgp_set_delete_comm_payload_contents['inline'] = \ + {'config': {'communities': set_extcommunity_delete_attrs}} + + request = { + 'path': bgp_set_delete_extcomm_uri, + 'method': PATCH, + 'data': bgp_set_delete_extcomm_payload + } + dict_delete_requests.append(request) + + # Check for deletion of ipv6_next_hop attributes. Delete the attributes + # in the currently configured ipv6_next_hop dict list if they exist. + # As an optimization, avoid deleting attributes that will be replaced + # by the received command. + ipv6_next_hop_deleted_members = {} + if 'ipv6_next_hop' not in cfg_set_top: + if command['set'].get('ipv6_next_hop'): + command['set'].pop('ipv6_next_hop') + if command['set'] is None: + command.pop('set') + return + else: + # Delete eligible configured ipv6_next_hop members. + cfg_ipv6_next_hop_key_set = set(cfg_set_top['ipv6_next_hop'].keys()) + cmd_ipv6_next_hop_key_set = ([]) + if cmd_set_top.get('ipv6_next_hop'): + cmd_ipv6_next_hop_key_set = set(cfg_set_top['ipv6_next_hop'].keys()) + command['set'].pop('ipv6_next_hop') + + set_uri = set_uri_attr['ipv6_next_hop'] + for ipv6_next_hop_key in cfg_ipv6_next_hop_key_set.difference(cmd_ipv6_next_hop_key_set): + ipv6_next_hop_deleted_members[ipv6_next_hop_key] = \ + cfg_set_top['ipv6_next_hop'][ipv6_next_hop_key] + request = {'path': set_uri[ipv6_next_hop_key], 'method': DELETE} + dict_delete_requests.append(request) + + if ipv6_next_hop_deleted_members: + # Update the list of deleted ipv6_next_hop attributes in the "command" dict. + if not cmd_set_top.get('ipv6_next_hop'): + command['set']['ipv6_next_hop'] = {} + command['set']['ipv6_next_hop'] = ipv6_next_hop_deleted_members + + if dict_delete_requests: + requests.extend(dict_delete_requests) + + return + + # If no top level attribute changes were requested, check for changes in + # dictionaries nested below the top level. + # ----------------------------------------------------------------------- + + # Check for replacement of set "community" lists. Delete the items in + # the currently configured list if it exists and any items for that + # list are specified in the received command. + dict_delete_requests = [] + set_community_delete_attrs = [] + if 'community' in cmd_set_top: + if 'community' not in cfg_set_top: + command['set'].pop('community') + if command['set'] is None: + command.pop('set') + return + else: + if 'community_number' in cmd_set_top['community']: + set_community_number_deletions = [] + if 'community_number' in cfg_set_top['community']: + symmetric_diff_set = \ + (set(cmd_set_top['community']['community_number']).symmetric_difference( + set(cfg_set_top['community']['community_number']))) + if symmetric_diff_set: + for community_number in cfg_set_top['community']['community_number']: + if (community_number not in cmd_set_top['community'] + ['community_number']): + set_community_delete_attrs.append(community_number) + set_community_number_deletions.append(community_number) + command['set']['community'].pop('community_number') + if set_community_delete_attrs: + command['set']['community']['community_number'] = \ + set_community_number_deletions + + if 'community_attributes' in cmd_set_top['community']: + set_community_named_attr_deletions = [] + if 'community_attributes' in cfg_set_top['community']: + symmetric_diff_set = \ + (set(cmd_set_top[ + 'community']['community_attributes']).symmetric_difference( + set(cfg_set_top['community']['community_attributes']))) + if symmetric_diff_set: + cfg_set_top_comm_attr = cfg_set_top['community']['community_attributes'] + for community_attr in cfg_set_top_comm_attr: + if (community_attr not in cmd_set_top['community'] + ['community_attributes']): + set_community_delete_attrs.append( + self.set_community_rest_names[community_attr]) + set_community_named_attr_deletions.append(community_attr) + command['set']['community'].pop('community_attributes') + if set_community_named_attr_deletions: + command['set']['community']['community_attributes'] = \ + set_community_named_attr_deletions + if command['set']['community'] is None: + command['set'].pop('community') + + # Format and enqueue a request to delete eligible community attributes + if set_community_delete_attrs: + bgp_set_delete_community_uri = bgp_set_delete_req_base + 'set-community' + bgp_set_delete_comm_payload = \ + {'openconfig-bgp-policy:set-community': {}} + bgp_set_delete_comm_payload_contents = \ + bgp_set_delete_comm_payload['openconfig-bgp-policy:set-community'] + bgp_set_delete_comm_payload_contents['config'] = \ + {'method': 'INLINE', 'options': 'REMOVE'} + bgp_set_delete_comm_payload_contents['inline'] = \ + {'config': {'communities': set_community_delete_attrs}} + + request = { + 'path': bgp_set_delete_community_uri, + 'method': PATCH, + 'data': bgp_set_delete_comm_payload + } + dict_delete_requests.append(request) + + # Check for replacement of set "extcommunity" lists. Delete any items in + # the currently configured list if the corresponding item is not + # specified in the received command. + set_extcommunity_delete_attrs = [] + if 'extcommunity' in cmd_set_top: + if 'extcommunity' not in cfg_set_top: + command['set'].pop('extcommunity') + else: + for extcomm_type in self.set_extcomm_rest_names: + set_extcommunity_delete_attrs_type = [] + if cmd_set_top['extcommunity'].get(extcomm_type): + if extcomm_type in cfg_set_top['extcommunity']: + symmetric_diff_set = \ + (set( + cmd_set_top['extcommunity'][extcomm_type]).symmetric_difference( + set(cfg_set_top['extcommunity'][extcomm_type]))) + if symmetric_diff_set: + # Append eligible entries to the delete list. + for extcomm_number in cfg_set_top['extcommunity'][extcomm_type]: + if (extcomm_number not in + cmd_set_top['extcommunity'][extcomm_type]): + set_extcommunity_delete_attrs.append( + self.set_extcomm_rest_names[extcomm_type] + + extcomm_number) + set_extcommunity_delete_attrs_type.append(extcomm_number) + # Replace the requested extcommunity numbers for this type with the list of + # deleted extcommunity numbers (if any) for this type. + command['set']['extcommunity'].pop(extcomm_type) + if set_extcommunity_delete_attrs_type: + command['set']['extcommunity'][extcomm_type] = \ + set_extcommunity_delete_attrs_type + + if command['set']['extcommunity'] is None: + command['set'].pop('extcommunity') + + # Format and enqueue a request to delete eligible extcommunity attributes + if set_extcommunity_delete_attrs: + bgp_set_delete_extcomm_uri = bgp_set_delete_req_base + 'set-ext-community' + bgp_set_delete_extcomm_payload = \ + {'openconfig-bgp-policy:set-ext-community': {}} + bgp_set_delete_comm_payload_contents = \ + bgp_set_delete_extcomm_payload[ + 'openconfig-bgp-policy:set-ext-community'] + bgp_set_delete_comm_payload_contents['config'] = \ + {'method': 'INLINE', 'options': 'REMOVE'} + bgp_set_delete_comm_payload_contents['inline'] = \ + {'config': {'communities': set_extcommunity_delete_attrs}} + + request = { + 'path': bgp_set_delete_extcomm_uri, + 'method': PATCH, + 'data': bgp_set_delete_extcomm_payload + } + dict_delete_requests.append(request) + + # If the "replaced" command set includes ipv6_next_hop attributes that + # differ from the currently configured attributes, delete + # ipv6_next_hop configuration, if it exists, for any ipv6_next hop + # attributes that are not specified in the received command. + if 'ipv6_next_hop' in cmd_set_top: + ipv6_next_hop_deleted_members = {} + if 'ipv6_next_hop' in cfg_set_top: + symmetric_diff_set = \ + (set(cmd_set_top['ipv6_next_hop'].keys()).symmetric_difference( + set(cfg_set_top['ipv6_next_hop'].keys()))) + intersection_diff_set = \ + (set(cmd_set_top['ipv6_next_hop'].keys()).intersection( + set(cfg_set_top['ipv6_next_hop'].keys()))) + if (symmetric_diff_set or + (any(keyname for keyname in intersection_diff_set if + cmd_set_top['ipv6_next_hop'][keyname] != + cfg_set_top['ipv6_next_hop'][keyname]))): + set_uri = set_uri_attr['ipv6_next_hop'] + for member_key in set_uri: + if (cfg_set_top['ipv6_next_hop'].get(member_key) is not None and + cmd_set_top['ipv6_next_hop'].get(member_key) is None): + ipv6_next_hop_deleted_members[member_key] = \ + cfg_set_top['ipv6_next_hop'][member_key] + request = {'path': set_uri[member_key], 'method': DELETE} + dict_delete_requests.append(request) + command['set'].pop('ipv6_next_hop') + if ipv6_next_hop_deleted_members: + command['set']['ipv6_next_hop'] = ipv6_next_hop_deleted_members + + if dict_delete_requests: + requests.extend(dict_delete_requests) + + def validate_and_normalize_config(self, input_config_list): + '''For each input route map dict in the input_config_list list, + remove empty entries, validate the contents of the dict against the + argspec constraints for route maps, and convert input interface names to + the format required for the currently configured interface naming + mode.''' + updated_config_list = remove_empties_from_list(input_config_list) + validate_config(self._module.argument_spec, {'config': updated_config_list}) + + # - Verify that parameters required for most "states" are present in + # each dict in the input list. + # - Check for interface names in the input configuration and + # perform any needed reformatting of the names. + for route_map in updated_config_list: + + # Verify the presence of a "sequence number" and "action" value + # for all states other than "deleted" + if self._module.params['state'] != 'deleted': + check_required(self._module, ['action', 'sequence_num'], route_map, ['config']) + + # Check for interface names requiring re-formatting. + if not route_map.get('match'): + continue + + if route_map['match'].get('interface'): + intf_name = route_map['match']['interface'] + updated_intf_name = get_normalize_interface_name(intf_name, self._module) + route_map['match']['interface'] = updated_intf_name + + if route_map['match'].get('peer') and route_map['match']['peer'].get('interface'): + intf_name = route_map['match']['peer']['interface'] + updated_intf_name = get_normalize_interface_name(intf_name, self._module) + route_map['match']['peer']['interface'] = updated_intf_name + + return updated_config_list 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 index 047357470..c3d62d852 100644 --- 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 @@ -28,6 +28,12 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( update_states, get_diff, + get_replaced_config, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff ) network_instance_path = '/data/openconfig-network-instance:network-instances/network-instance' @@ -41,6 +47,61 @@ TEST_KEYS = [ {'next_hops': {'index': ''}}, ] +is_delete_all = False + + +def __derive_static_route_next_hop_config_key_match_op(key_set, command, exist_conf): + bh = command['index'].get('blackhole', None) + itf = command['index'].get('interface', None) + nv = command['index'].get('nexthop_vrf', None) + nh = command['index'].get('next_hop', None) + conf_bh = exist_conf['index'].get('blackhole', None) + conf_itf = exist_conf['index'].get('interface', None) + conf_nv = exist_conf['index'].get('nexthop_vrf', None) + conf_nh = exist_conf['index'].get('next_hop', None) + + if bh == conf_bh and itf == conf_itf and nv == conf_nv and nh == conf_nh: + return True + else: + return False + + +def __derive_static_route_next_hop_config_delete_op(key_set, command, exist_conf): + new_conf = [] + + if is_delete_all: + return True, new_conf + + metric = command.get('metric', None) + tag = command.get('tag', None) + track = command.get('track', None) + + if metric is None and tag is None and track is None: + return True, new_conf + + new_conf = exist_conf + + conf_metric = new_conf.get('metric', None) + conf_tag = new_conf.get('tag', None) + conf_track = new_conf.get('track', None) + + if metric == conf_metric: + new_conf['metric'] = None + if tag == conf_tag: + new_conf['tag'] = None + if track == conf_track: + new_conf['track'] = None + + return True, new_conf + + +TEST_KEYS_formatted_diff = [ + {'config': {'vrf_name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'static_list': {'prefix': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'next_hops': {'index': '', '__delete_op': __derive_static_route_next_hop_config_delete_op, + '__key_match_op': __derive_static_route_next_hop_config_key_match_op}} +] + class Static_routes(ConfigBase): """ @@ -97,6 +158,21 @@ class Static_routes(ConfigBase): if result['changed']: result['after'] = changed_static_routes_facts + new_config = changed_static_routes_facts + old_config = existing_static_routes_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_static_routes_facts, + TEST_KEYS_formatted_diff) + self.post_process_generated_config(new_config) + result['after(generated)'] = new_config + + if self._module._diff: + self.sort_lists_in_config(new_config) + self.sort_lists_in_config(old_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -132,10 +208,14 @@ class Static_routes(ConfigBase): if state == 'deleted': commands, requests = self._state_deleted(want, have, diff) elif state == 'merged': - commands, requests = self._state_merged(want, have, diff) + commands, requests = self._state_merged(diff) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have, diff) return commands, requests - def _state_merged(self, want, have, diff): + def _state_merged(self, diff): """ The command generator when state is merged :rtype: A list @@ -143,7 +223,7 @@ class Static_routes(ConfigBase): the current configuration """ commands = diff - requests = self.get_modify_static_routes_requests(commands, have) + requests = self.get_modify_static_routes_requests(commands) if commands and len(requests) > 0: commands = update_states(commands, "merged") @@ -159,6 +239,7 @@ class Static_routes(ConfigBase): :returns: the commands necessary to remove the current configuration of the provided objects """ + global is_delete_all is_delete_all = False # if want is none, then delete ALL if not want: @@ -176,7 +257,73 @@ class Static_routes(ConfigBase): return commands, requests - def get_modify_static_routes_requests(self, commands, have): + def _state_overridden(self, want, have): + """ The command generator when state is overridden + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + global is_delete_all + + commands = [] + requests = [] + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + + if have and have != want: + is_delete_all = True + del_requests = self.get_delete_static_routes_requests(have, None, is_delete_all) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + mod_commands = want + mod_requests = self.get_modify_static_routes_requests(mod_commands) + + if len(mod_requests) > 0: + requests.extend(mod_requests) + commands.extend(update_states(mod_commands, "overridden")) + + 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 + """ + global is_delete_all + + commands = [] + requests = [] + replaced_config = get_replaced_config(want, have, TEST_KEYS) + + mod_commands = [] + if replaced_config: + self.sort_lists_in_config(replaced_config) + self.sort_lists_in_config(have) + is_delete_all = (replaced_config == have) + del_requests = self.get_delete_static_routes_requests(replaced_config, have, is_delete_all) + requests.extend(del_requests) + commands.extend(update_states(replaced_config, "deleted")) + mod_commands = want + else: + mod_commands = diff + + if mod_commands: + mod_requests = self.get_modify_static_routes_requests(mod_commands) + + if len(mod_requests) > 0: + requests.extend(mod_requests) + commands.extend(update_states(mod_commands, "replaced")) + + return commands, requests + + def get_modify_static_routes_requests(self, commands): requests = [] if not commands: @@ -208,7 +355,7 @@ class Static_routes(ConfigBase): idx = self.generate_index(index) if idx: next_hop_cfg['index'] = idx - if blackhole: + if blackhole is not None: next_hop_cfg['blackhole'] = blackhole if nexthop_vrf: next_hop_cfg['network-instance'] = nexthop_vrf @@ -342,3 +489,33 @@ class Static_routes(ConfigBase): request = {'path': url, 'method': DELETE} return request + + def sort_lists_in_config(self, config): + if config: + config.sort(key=self.get_vrf_name) + for cfg in config: + if 'static_list' in cfg and cfg['static_list']: + cfg['static_list'].sort(key=self.get_prefix) + for rt in cfg['static_list']: + if 'next_hops' in rt and rt['next_hops']: + rt['next_hops'].sort(key=lambda x: (x['index'].get('blackhole', None) is not None, + x['index'].get('interface', None) is not None, + x['index'].get('nexthop_vrf', None) is not None, + x['index'].get('next_hop', None) is not None)) + + def get_vrf_name(self, vrf_name): + return vrf_name.get('vrf_name') + + def get_prefix(self, prefix): + return prefix.get('prefix') + + def post_process_generated_config(self, configs): + for conf in configs[:]: + sls = conf.get('static_list', []) + if sls: + for sl in sls[:]: + if not sl.get('next_hops', []): + sls.remove(sl) + + if not conf.get('static_list', []): + configs.remove(conf) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/stp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/stp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/stp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/stp/stp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/stp/stp.py new file mode 100644 index 000000000..031c794ae --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/stp/stp.py @@ -0,0 +1,1404 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_stp 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 copy import deepcopy +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_ranges_in_list, + get_diff, + remove_empties, +) +from ansible.module_utils.connection import ConnectionError + + +PATCH = 'patch' +DELETE = 'delete' +TEST_KEYS = [ + {'interfaces': {'intf_name': ''}}, + {'mst_instances': {'mst_id': ''}}, + {'pvst': {'vlan_id': ''}}, + {'rapid_pvst': {'vlan_id': ''}}, +] +STP_PATH = 'data/openconfig-spanning-tree:stp' +stp_map = { + True: 'EDGE_ENABLE', + False: 'EDGE_DISABLE', + 'mst': 'MSTP', + 'pvst': 'PVST', + 'rapid_pvst': 'RAPID_PVST', + 'point-to-point': 'P2P', + 'shared': 'SHARED', + 'loop': 'LOOP', + 'root': 'ROOT', + 'none': 'NONE' +} + + +class Stp(ConfigBase): + """ + The sonic_stp class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'stp', + ] + + def __init__(self, module): + super(Stp, self).__init__(module) + + def get_stp_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) + stp_facts = facts['ansible_network_resources'].get('stp') + if not stp_facts: + return [] + return stp_facts + + def execute_module(self): + """ Execute the module + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = [] + commands = [] + + existing_stp_facts = self.get_stp_facts() + commands, requests = self.set_config(existing_stp_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_stp_facts = self.get_stp_facts() + + result['before'] = existing_stp_facts + if result['changed']: + result['after'] = changed_stp_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_stp_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_stp_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) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(diff, have) + 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 + """ + commands = [] + mod_commands = [] + replaced_config, requests = self.get_replaced_config(want, have) + + if replaced_config: + commands.extend(update_states(replaced_config, "deleted")) + mod_commands = want + else: + mod_commands = diff + + if mod_commands: + mod_requests = self.get_modify_stp_requests(mod_commands, have) + if len(mod_requests) > 0: + requests.extend(mod_requests) + commands.extend(update_states(mod_commands, "replaced")) + return commands, requests + + def _state_overridden(self, want, have): + """ 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 = [] + del_commands = get_diff(have, want, TEST_KEYS) + self.remove_default_entries(del_commands) + del_commands = remove_empties(del_commands) + + if del_commands: + is_delete_all = True + del_requests = self.get_delete_stp_requests(del_commands, have, is_delete_all) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = {} + + if not have and want: + mod_commands = want + mod_requests = self.get_modify_stp_requests(mod_commands, have) + + if len(mod_requests) > 0: + requests.extend(mod_requests) + commands.extend(update_states(mod_commands, "overridden")) + return commands, requests + + def _state_merged(self, diff, have): + """ 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_stp_requests(commands, have) + commands = remove_empties(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 + """ + is_delete_all = False + want = remove_empties(want) + + if not want: + commands = deepcopy(have) + is_delete_all = True + else: + commands = deepcopy(want) + + self.remove_default_entries(commands) + commands = remove_empties(commands) + requests = self.get_delete_stp_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_stp_requests(self, commands, have): + requests = [] + + if not commands: + return requests + + global_request = self.get_modify_stp_global_request(commands, have) + interfaces_request = self.get_modify_stp_interfaces_request(commands) + mstp_requests = self.get_modify_stp_mstp_request(commands, have) + pvst_request = self.get_modify_stp_pvst_request(commands) + rapid_pvst_request = self.get_modify_stp_rapid_pvst_request(commands) + + if global_request: + requests.append(global_request) + if interfaces_request: + requests.append(interfaces_request) + if mstp_requests: + requests.append(mstp_requests) + if pvst_request: + requests.append(pvst_request) + if rapid_pvst_request: + requests.append(rapid_pvst_request) + + return requests + + def get_modify_stp_global_request(self, commands, have): + request = None + + if not commands: + return request + + stp_global = commands.get('global', None) + if stp_global: + global_dict = {} + config_dict = {} + enabled_protocol = stp_global.get('enabled_protocol', None) + loop_guard = stp_global.get('loop_guard', None) + bpdu_filter = stp_global.get('bpdu_filter', None) + disabled_vlans = stp_global.get('disabled_vlans', None) + root_guard_timeout = stp_global.get('root_guard_timeout', None) + portfast = stp_global.get('portfast', None) + hello_time = stp_global.get('hello_time', None) + max_age = stp_global.get('max_age', None) + fwd_delay = stp_global.get('fwd_delay', None) + bridge_priority = stp_global.get('bridge_priority', None) + + if enabled_protocol: + config_dict['enabled-protocol'] = [stp_map[enabled_protocol]] + if loop_guard is not None: + config_dict['loop-guard'] = loop_guard + if bpdu_filter is not None: + config_dict['bpdu-filter'] = bpdu_filter + if disabled_vlans: + if have: + cfg_stp_global = have.get('global', None) + if cfg_stp_global: + cfg_disabled_vlans = cfg_stp_global.get('disabled_vlans', None) + if cfg_disabled_vlans: + disabled_vlans = self.get_vlans_diff(disabled_vlans, cfg_disabled_vlans) + if not disabled_vlans: + commands['global'].pop('disabled_vlans') + if disabled_vlans: + config_dict['openconfig-spanning-tree-ext:disabled-vlans'] = self.convert_vlans_list(disabled_vlans) + if root_guard_timeout: + config_dict['openconfig-spanning-tree-ext:rootguard-timeout'] = root_guard_timeout + if portfast is not None and enabled_protocol == 'pvst': + config_dict['openconfig-spanning-tree-ext:portfast'] = portfast + elif portfast: + self._module.fail_json(msg='Portfast only configurable for pvst protocol.') + if hello_time: + config_dict['openconfig-spanning-tree-ext:hello-time'] = hello_time + if max_age: + config_dict['openconfig-spanning-tree-ext:max-age'] = max_age + if fwd_delay: + config_dict['openconfig-spanning-tree-ext:forwarding-delay'] = fwd_delay + if bridge_priority: + config_dict['openconfig-spanning-tree-ext:bridge-priority'] = bridge_priority + if config_dict: + global_dict['config'] = config_dict + url = '%s/global' % (STP_PATH) + payload = {'openconfig-spanning-tree:global': global_dict} + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_stp_interfaces_request(self, commands): + request = None + interfaces = commands.get('interfaces', None) + + if interfaces: + intf_list = [] + for intf in interfaces: + intf_dict = {} + config_dict = {} + intf_name = intf.get('intf_name', None) + edge_port = intf.get('edge_port', None) + link_type = intf.get('link_type', None) + guard = intf.get('guard', None) + bpdu_guard = intf.get('bpdu_guard', None) + bpdu_filter = intf.get('bpdu_filter', None) + portfast = intf.get('portfast', None) + uplink_fast = intf.get('uplink_fast', None) + shutdown = intf.get('shutdown', None) + cost = intf.get('cost', None) + port_priority = intf.get('port_priority', None) + stp_enable = intf.get('stp_enable', None) + + if intf_name: + config_dict['name'] = intf_name + if edge_port is not None: + config_dict['edge-port'] = stp_map[edge_port] + if link_type: + config_dict['link-type'] = stp_map[link_type] + if guard: + config_dict['guard'] = stp_map[guard] + if bpdu_guard is not None: + config_dict['bpdu-guard'] = bpdu_guard + if bpdu_filter is not None: + config_dict['bpdu-filter'] = bpdu_filter + if portfast is not None: + config_dict['openconfig-spanning-tree-ext:portfast'] = portfast + if uplink_fast is not None: + config_dict['openconfig-spanning-tree-ext:uplink-fast'] = uplink_fast + if shutdown is not None: + config_dict['openconfig-spanning-tree-ext:bpdu-guard-port-shutdown'] = shutdown + if cost: + config_dict['openconfig-spanning-tree-ext:cost'] = cost + if port_priority: + config_dict['openconfig-spanning-tree-ext:port-priority'] = port_priority + if stp_enable is not None: + config_dict['openconfig-spanning-tree-ext:spanning-tree-enable'] = stp_enable + if config_dict: + intf_dict['name'] = intf_name + intf_dict['config'] = config_dict + intf_list.append(intf_dict) + if intf_list: + url = '%s/interfaces' % (STP_PATH) + payload = {'openconfig-spanning-tree:interfaces': {'interface': intf_list}} + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_stp_mstp_request(self, commands, have): + request = None + + if not commands: + return request + + mstp = commands.get('mstp', None) + + if mstp: + mstp_dict = {} + config_dict = {} + mst_name = mstp.get('mst_name', None) + revision = mstp.get('revision', None) + max_hop = mstp.get('max_hop', None) + hello_time = mstp.get('hello_time', None) + max_age = mstp.get('max_age', None) + fwd_delay = mstp.get('fwd_delay', None) + mst_instances = mstp.get('mst_instances', None) + + if mst_name: + config_dict['name'] = mst_name + if revision: + config_dict['revision'] = revision + if max_hop: + config_dict['max-hop'] = max_hop + if hello_time: + config_dict['hello-time'] = hello_time + if max_age: + config_dict['max-age'] = max_age + if fwd_delay: + config_dict['forwarding-delay'] = fwd_delay + if mst_instances: + mst_inst_list = [] + pop_list = [] + for mst in mst_instances: + mst_inst_dict = {} + mst_cfg_dict = {} + mst_index = mst_instances.index(mst) + mst_id = mst.get('mst_id', None) + bridge_priority = mst.get('bridge_priority', None) + interfaces = mst.get('interfaces', None) + vlans = mst.get('vlans', None) + + if mst_id: + mst_cfg_dict['mst-id'] = mst_id + if bridge_priority: + mst_cfg_dict['bridge-priority'] = bridge_priority + if interfaces: + intf_list = self.get_interfaces_list(interfaces) + if intf_list: + mst_inst_dict['interfaces'] = {'interface': intf_list} + if vlans: + if have: + cfg_mstp = have.get('mstp', None) + if cfg_mstp: + cfg_mst_instances = cfg_mstp.get('mst_instances', None) + if cfg_mst_instances: + for cfg_mst in cfg_mst_instances: + cfg_mst_id = cfg_mst.get('mst_id', None) + cfg_vlans = cfg_mst.get('vlans', None) + + if mst_id == cfg_mst_id and cfg_vlans: + vlans = self.get_vlans_diff(vlans, cfg_vlans) + if not vlans: + pop_list.insert(0, mst_index) + if vlans: + mst_cfg_dict['vlan'] = self.convert_vlans_list(vlans) + if mst_cfg_dict: + mst_inst_dict['mst-id'] = mst_id + mst_inst_dict['config'] = mst_cfg_dict + if mst_inst_dict: + mst_inst_list.append(mst_inst_dict) + if pop_list: + for i in pop_list: + commands['mstp']['mst_instances'][i].pop('vlans') + if mst_inst_list: + mstp_dict['mst-instances'] = {'mst-instance': mst_inst_list} + + if config_dict: + mstp_dict['config'] = config_dict + + if mstp_dict: + url = '%s/mstp' % (STP_PATH) + payload = {'openconfig-spanning-tree:mstp': mstp_dict} + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_stp_pvst_request(self, commands): + request = None + pvst = commands.get('pvst', None) + + if pvst: + vlans_list = self.get_vlans_list(pvst) + if vlans_list: + url = '%s/openconfig-spanning-tree-ext:pvst' % (STP_PATH) + payload = {'openconfig-spanning-tree-ext:pvst': {'vlans': vlans_list}} + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_modify_stp_rapid_pvst_request(self, commands): + request = None + rapid_pvst = commands.get('rapid_pvst', None) + + if rapid_pvst: + vlans_list = self.get_vlans_list(rapid_pvst) + if vlans_list: + url = '%s/rapid-pvst' % (STP_PATH) + payload = {'openconfig-spanning-tree:rapid-pvst': {'vlan': vlans_list}} + request = {'path': url, 'method': PATCH, 'data': payload} + + return request + + def get_vlans_list(self, data): + vlans_list = [] + + for vlan in data: + vlans_dict = {} + config_dict = {} + vlan_id = vlan.get('vlan_id', None) + hello_time = vlan.get('hello_time', None) + max_age = vlan.get('max_age', None) + fwd_delay = vlan.get('fwd_delay', None) + bridge_priority = vlan.get('bridge_priority', None) + interfaces = vlan.get('interfaces', None) + + if vlan_id: + config_dict['vlan-id'] = vlan_id + if hello_time: + config_dict['hello-time'] = hello_time + if max_age: + config_dict['max-age'] = max_age + if fwd_delay: + config_dict['forwarding-delay'] = fwd_delay + if bridge_priority: + config_dict['bridge-priority'] = bridge_priority + if interfaces: + intf_list = self.get_interfaces_list(interfaces) + if intf_list: + vlans_dict['interfaces'] = {'interface': intf_list} + if config_dict: + vlans_dict['vlan-id'] = vlan_id + vlans_dict['config'] = config_dict + if vlans_dict: + vlans_list.append(vlans_dict) + + return vlans_list + + def get_interfaces_list(self, interfaces): + intf_list = [] + for intf in interfaces: + intf_dict = {} + intf_cfg_dict = {} + intf_name = intf.get('intf_name', None) + cost = intf.get('cost', None) + port_priority = intf.get('port_priority', None) + + if intf_name: + intf_cfg_dict['name'] = intf_name + if cost: + intf_cfg_dict['cost'] = cost + if port_priority: + intf_cfg_dict['port-priority'] = port_priority + if intf_cfg_dict: + intf_dict['name'] = intf_name + intf_dict['config'] = intf_cfg_dict + intf_list.append(intf_dict) + + return intf_list + + def get_vlans_common(self, vlans, cfg_vlans): + """Returns the vlan ranges that are common in the want and have + vlans lists + """ + vlans = self.get_vlan_id_list(vlans) + cfg_vlans = self.get_vlan_id_list(cfg_vlans) + return self.get_vlan_range_list(list(set(vlans).intersection(set(cfg_vlans)))) + + def get_vlans_diff(self, vlans, cfg_vlans): + """Returns the vlan ranges present only in the want vlans list + and not in the have vlans list + """ + vlans = self.get_vlan_id_list(vlans) + cfg_vlans = self.get_vlan_id_list(cfg_vlans) + return self.get_vlan_range_list(list(set(vlans) - set(cfg_vlans))) + + @staticmethod + def get_vlan_id_list(vlans): + """Returns a list of all VLAN IDs specified in a vlans list""" + vlan_id_list = [] + + if vlans: + for vlan_val in vlans: + if '-' in vlan_val or '..' in vlan_val: + start, end = re.split(r'-|\.\.', vlan_val) + vlan_id_list.extend(range(int(start), int(end) + 1)) + else: + # Single VLAN ID + vlan_id_list.append(int(vlan_val)) + + return vlan_id_list + + @staticmethod + def get_vlan_range_list(vlan_id_list): + """Returns the vlans list for a given list of VLAN IDs""" + vlan_range_list = [] + + if vlan_id_list: + vlan_id_list.sort() + for vlan_range in get_ranges_in_list(vlan_id_list): + vlan_range_list.append('-'.join(map(str, (vlan_range[0], vlan_range[-1])[:len(vlan_range)]))) + + return vlan_range_list + + def convert_vlans_list(self, vlans): + converted_vlans = [] + + for vlan in vlans: + if len(vlan) == 1: + converted_vlans.append(int(vlan)) + else: + converted_vlans.append(vlan.replace('-', '..')) + + return converted_vlans + + def get_delete_stp_requests(self, commands, have, is_delete_all): + requests = [] + + if not commands: + return requests + + if is_delete_all: + requests.append(self.get_delete_all_stp_request()) + else: + requests.extend(self.get_delete_stp_mstp_requests(commands, have)) + requests.extend(self.get_delete_stp_pvst_requests(commands, have)) + requests.extend(self.get_delete_stp_rapid_pvst_requests(commands, have)) + requests.extend(self.get_delete_stp_interfaces_requests(commands, have)) + requests.extend(self.get_delete_stp_global_requests(commands, have)) + + return requests + + def get_delete_stp_global_requests(self, commands, have): + requests = [] + + stp_global = commands.get('global', None) + if stp_global: + enabled_protocol = stp_global.get('enabled_protocol', None) + loop_guard = stp_global.get('loop_guard', None) + bpdu_filter = stp_global.get('bpdu_filter', None) + disabled_vlans = stp_global.get('disabled_vlans', None) + root_guard_timeout = stp_global.get('root_guard_timeout', None) + portfast = stp_global.get('portfast', None) + hello_time = stp_global.get('hello_time', None) + max_age = stp_global.get('max_age', None) + fwd_delay = stp_global.get('fwd_delay', None) + bridge_priority = stp_global.get('bridge_priority', None) + + cfg_stp_global = have.get('global', None) + if cfg_stp_global: + cfg_enabled_protocol = cfg_stp_global.get('enabled_protocol', None) + cfg_loop_guard = cfg_stp_global.get('loop_guard', None) + cfg_bpdu_filter = cfg_stp_global.get('bpdu_filter', None) + cfg_disabled_vlans = cfg_stp_global.get('disabled_vlans', None) + cfg_root_guard_timeout = cfg_stp_global.get('root_guard_timeout', None) + cfg_portfast = cfg_stp_global.get('portfast', None) + cfg_hello_time = cfg_stp_global.get('hello_time', None) + cfg_max_age = cfg_stp_global.get('max_age', None) + cfg_fwd_delay = cfg_stp_global.get('fwd_delay', None) + cfg_bridge_priority = cfg_stp_global.get('bridge_priority', None) + + # Default loop_guard is false, don't delete if false + if loop_guard and loop_guard == cfg_loop_guard: + requests.append(self.get_delete_stp_global_attr('loop-guard')) + # Default bpdu_filter is false, don't delete if false + if bpdu_filter and bpdu_filter == cfg_bpdu_filter: + requests.append(self.get_delete_stp_global_attr('bpdu-filter')) + if disabled_vlans and cfg_disabled_vlans: + disabled_vlans_to_delete = self.get_vlans_common(disabled_vlans, cfg_disabled_vlans) + for i, vlan in enumerate(disabled_vlans_to_delete): + if '-' in vlan: + disabled_vlans_to_delete[i] = vlan.replace('-', '..') + if disabled_vlans_to_delete: + encoded_vlans = '%2C'.join(disabled_vlans_to_delete) + attr = 'openconfig-spanning-tree-ext:disabled-vlans=%s' % (encoded_vlans) + requests.append(self.get_delete_stp_global_attr(attr)) + else: + commands['global'].pop('disabled_vlans') + if root_guard_timeout: + if root_guard_timeout == cfg_root_guard_timeout: + requests.append(self.get_delete_stp_global_attr('openconfig-spanning-tree-ext:rootguard-timeout')) + else: + commands['global'].pop('root_guard_timeout') + # Default portfast is false, don't delete if false + if portfast and portfast == cfg_portfast: + requests.append(self.get_delete_stp_global_attr('openconfig-spanning-tree-ext:portfast')) + if hello_time and hello_time == cfg_hello_time: + requests.append(self.get_delete_stp_global_attr('openconfig-spanning-tree-ext:hello-time')) + if max_age and max_age == cfg_max_age: + requests.append(self.get_delete_stp_global_attr('openconfig-spanning-tree-ext:max-age')) + if fwd_delay and fwd_delay == cfg_fwd_delay: + requests.append(self.get_delete_stp_global_attr('openconfig-spanning-tree-ext:forwarding-delay')) + if bridge_priority and bridge_priority == cfg_bridge_priority: + requests.append(self.get_delete_stp_global_attr('openconfig-spanning-tree-ext:bridge-priority')) + if enabled_protocol: + if enabled_protocol == cfg_enabled_protocol: + requests.append(self.get_delete_stp_global_attr('enabled-protocol')) + else: + commands['global'].pop('enabled_protocol') + + return requests + + def get_delete_stp_interfaces_requests(self, commands, have): + requests = [] + + interfaces = commands.get('interfaces', None) + if interfaces: + intf_list = [] + for intf in interfaces: + intf_dict = {} + intf_name = intf.get('intf_name', None) + edge_port = intf.get('edge_port', None) + link_type = intf.get('link_type', None) + guard = intf.get('guard', None) + bpdu_guard = intf.get('bpdu_guard', None) + bpdu_filter = intf.get('bpdu_filter', None) + portfast = intf.get('portfast', None) + uplink_fast = intf.get('uplink_fast', None) + shutdown = intf.get('shutdown', None) + cost = intf.get('cost', None) + port_priority = intf.get('port_priority', None) + stp_enable = intf.get('stp_enable', None) + + cfg_interfaces = have.get('interfaces', None) + if cfg_interfaces: + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + cfg_edge_port = cfg_intf.get('edge_port', None) + cfg_link_type = cfg_intf.get('link_type', None) + cfg_guard = cfg_intf.get('guard', None) + cfg_bpdu_guard = cfg_intf.get('bpdu_guard', None) + cfg_bpdu_filter = cfg_intf.get('bpdu_filter', None) + cfg_portfast = cfg_intf.get('portfast', None) + cfg_uplink_fast = cfg_intf.get('uplink_fast', None) + cfg_shutdown = cfg_intf.get('shutdown', None) + cfg_cost = cfg_intf.get('cost', None) + cfg_port_priority = cfg_intf.get('port_priority', None) + cfg_stp_enable = cfg_intf.get('stp_enable', None) + + if intf_name and intf_name == cfg_intf_name: + # Default edge_port is false, don't delete if false + if edge_port and edge_port == cfg_edge_port: + requests.append(self.get_delete_stp_interface_attr(intf_name, 'edge-port')) + intf_dict.update({'intf_name': intf_name, 'edge_port': edge_port}) + if link_type and link_type == cfg_link_type: + requests.append(self.get_delete_stp_interface_attr(intf_name, 'link-type')) + intf_dict.update({'intf_name': intf_name, 'link_type': link_type}) + if guard and guard == cfg_guard: + requests.append(self.get_delete_stp_interface_attr(intf_name, 'guard')) + intf_dict.update({'intf_name': intf_name, 'guard': guard}) + # Default bpdu_guard is false, don't delete if false + if bpdu_guard and bpdu_guard == cfg_bpdu_guard: + url = '%s/interfaces/interface=%s/config/bpdu-guard' % (STP_PATH, intf_name) + payload = {'openconfig-spanning-tree:bpdu-guard': False} + request = {'path': url, 'method': PATCH, 'data': payload} + requests.append(request) + intf_dict.update({'intf_name': intf_name, 'bpdu_guard': bpdu_guard}) + # Default bpdu_filter is false, don't delete if false + if bpdu_filter and bpdu_filter == cfg_bpdu_filter: + requests.append(self.get_delete_stp_interface_attr(intf_name, 'bpdu-filter')) + intf_dict.update({'intf_name': intf_name, 'bpdu_filter': bpdu_filter}) + # Default portfast is false, don't delete if false + if portfast and portfast == cfg_portfast: + requests.append(self.get_delete_stp_interface_attr(intf_name, 'openconfig-spanning-tree-ext:portfast')) + intf_dict.update({'intf_name': intf_name, 'portfast': portfast}) + # Default uplink_fast is false, don't delete if false + if uplink_fast and uplink_fast == cfg_uplink_fast: + url = '%s/interfaces/interface=%s/config/openconfig-spanning-tree-ext:uplink-fast' % (STP_PATH, intf_name) + payload = {'openconfig-spanning-tree-ext:uplink-fast': False} + request = {'path': url, 'method': PATCH, 'data': payload} + requests.append(request) + intf_dict.update({'intf_name': intf_name, 'uplink_fast': uplink_fast}) + # Default shutdown is false, don't delete if false + if shutdown and shutdown == cfg_shutdown: + url = '%s/interfaces/interface=%s/config/openconfig-spanning-tree-ext:bpdu-guard-port-shutdown' % (STP_PATH, intf_name) + payload = {'openconfig-spanning-tree-ext:bpdu-guard-port-shutdown': False} + request = {'path': url, 'method': PATCH, 'data': payload} + requests.append(request) + intf_dict.update({'intf_name': intf_name, 'shutdown': shutdown}) + if cost and cost == cfg_cost: + requests.append(self.get_delete_stp_interface_attr(intf_name, 'openconfig-spanning-tree-ext:cost')) + intf_dict.update({'intf_name': intf_name, 'cost': cost}) + if port_priority and port_priority == cfg_port_priority: + requests.append(self.get_delete_stp_interface_attr(intf_name, 'openconfig-spanning-tree-ext:port-priority')) + intf_dict.update({'intf_name': intf_name, 'port_priority': port_priority}) + # Default stp_enable is true, don't delete if true + if stp_enable is False and stp_enable == cfg_stp_enable: + url = '%s/interfaces/interface=%s/config/openconfig-spanning-tree-ext:spanning-tree-enable' % (STP_PATH, intf_name) + payload = {'openconfig-spanning-tree-ext:spanning-tree-enable': True} + request = {'path': url, 'method': PATCH, 'data': payload} + requests.append(request) + intf_dict.update({'intf_name': intf_name, 'stp_enable': stp_enable}) + if (edge_port is None and not link_type and not guard and bpdu_guard is None and bpdu_filter is None and portfast is None and + uplink_fast is None and shutdown is None and not cost and not port_priority and stp_enable is None): + requests.append(self.get_delete_stp_interface(intf_name)) + intf_dict.update({'intf_name': intf_name}) + if intf_dict: + intf_list.append(intf_dict) + if intf_list: + commands['interfaces'] = intf_list + else: + commands.pop('interfaces') + + return requests + + def get_delete_stp_mstp_requests(self, commands, have): + requests = [] + + mstp = commands.get('mstp', None) + if mstp: + mst_name = mstp.get('mst_name', None) + revision = mstp.get('revision', None) + max_hop = mstp.get('max_hop', None) + hello_time = mstp.get('hello_time', None) + max_age = mstp.get('max_age', None) + fwd_delay = mstp.get('fwd_delay', None) + mst_instances = mstp.get('mst_instances', None) + + cfg_mstp = have.get('mstp', None) + if cfg_mstp: + cfg_mst_name = cfg_mstp.get('mst_name', None) + cfg_revision = cfg_mstp.get('revision', None) + cfg_max_hop = cfg_mstp.get('max_hop', None) + cfg_hello_time = cfg_mstp.get('hello_time', None) + cfg_max_age = cfg_mstp.get('max_age', None) + cfg_fwd_delay = cfg_mstp.get('fwd_delay', None) + cfg_mst_instances = cfg_mstp.get('mst_instances', None) + + if mst_name: + if mst_name == cfg_mst_name: + requests.append(self.get_delete_stp_mstp_cfg_attr('name')) + else: + commands['mstp'].pop('mst_name') + if revision: + if revision == cfg_revision: + requests.append(self.get_delete_stp_mstp_cfg_attr('revision')) + else: + commands['mstp'].pop('revision') + if max_hop: + if max_hop == cfg_max_hop: + requests.append(self.get_delete_stp_mstp_cfg_attr('max-hop')) + else: + commands['mstp'].pop('max_hop') + if hello_time: + if hello_time == cfg_hello_time: + requests.append(self.get_delete_stp_mstp_cfg_attr('hello-time')) + else: + commands['mstp'].pop('hello_time') + if max_age: + if max_age == cfg_max_age: + requests.append(self.get_delete_stp_mstp_cfg_attr('max-age')) + else: + commands['mstp'].pop('max_age') + if fwd_delay: + if fwd_delay == cfg_fwd_delay: + requests.append(self.get_delete_stp_mstp_cfg_attr('forwarding-delay')) + else: + commands['mstp'].pop('fwd_delay') + if mst_instances: + mst_inst_list = [] + for mst in mst_instances: + mst_inst_dict = {} + mst_id = mst.get('mst_id', None) + bridge_priority = mst.get('bridge_priority', None) + interfaces = mst.get('interfaces', None) + vlans = mst.get('vlans', None) + if cfg_mst_instances: + for cfg_mst in cfg_mst_instances: + cfg_mst_id = cfg_mst.get('mst_id', None) + cfg_bridge_priority = cfg_mst.get('bridge_priority', None) + cfg_interfaces = cfg_mst.get('interfaces', None) + cfg_vlans = cfg_mst.get('vlans', None) + + if mst_id == cfg_mst_id: + if bridge_priority and bridge_priority == cfg_bridge_priority: + requests.append(self.get_delete_mst_inst_cfg_attr(mst_id, 'bridge-priority')) + mst_inst_dict.update({'mst_id': mst_id, 'bridge_priority': bridge_priority}) + if interfaces: + intf_list = [] + for intf in interfaces: + intf_dict = {} + intf_name = intf.get('intf_name', None) + cost = intf.get('cost', None) + port_priority = intf.get('port_priority', None) + + if cfg_interfaces: + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + cfg_cost = cfg_intf.get('cost', None) + cfg_port_priority = cfg_intf.get('port_priority', None) + + if intf_name == cfg_intf_name: + if cost and cost == cfg_cost: + requests.append(self.get_delete_mst_intf_cfg_attr(mst_id, intf_name, 'cost')) + intf_dict.update({'intf_name': intf_name, 'cost': cost}) + if port_priority and port_priority == cfg_port_priority: + requests.append(self.get_delete_mst_intf_cfg_attr(mst_id, intf_name, 'port-priority')) + intf_dict.update({'intf_name': intf_name, 'port_priority': port_priority}) + if not cost and not port_priority: + requests.append(self.get_delete_mst_intf(mst_id, intf_name)) + intf_dict.update({'intf_name': intf_name}) + if intf_dict: + intf_list.append(intf_dict) + if intf_list: + mst_inst_dict.update({'mst_id': mst_id, 'interfaces': intf_list}) + + if vlans and cfg_vlans: + vlans_to_delete = self.get_vlans_common(vlans, cfg_vlans) + cmd_vlans = deepcopy(vlans_to_delete) + for i, vlan in enumerate(vlans_to_delete): + if '-' in vlan: + vlans_to_delete[i] = vlan.replace('-', '..') + if vlans_to_delete: + encoded_vlans = '%2C'.join(vlans_to_delete) + attr = 'vlan=%s' % (encoded_vlans) + requests.append(self.get_delete_mst_inst_cfg_attr(mst_id, attr)) + mst_inst_dict.update({'mst_id': mst_id, 'vlans': cmd_vlans}) + if not bridge_priority and not vlans and not interfaces: + requests.append(self.get_delete_mst_inst(mst_id)) + mst_inst_dict.update({'mst_id': mst_id}) + if mst_inst_dict: + mst_inst_list.append(mst_inst_dict) + if mst_inst_list: + commands['mstp']['mst_instances'] = mst_inst_list + else: + commands['mstp'].pop('mst_instances') + if not commands['mstp']: + commands.pop('mstp') + + return requests + + def get_delete_stp_pvst_requests(self, commands, have): + requests = [] + + pvst = commands.get('pvst', None) + if pvst: + vlans_list = [] + for vlan in pvst: + vlans_dict = {} + vlan_id = vlan.get('vlan_id', None) + hello_time = vlan.get('hello_time', None) + max_age = vlan.get('max_age', None) + fwd_delay = vlan.get('fwd_delay', None) + bridge_priority = vlan.get('bridge_priority', None) + interfaces = vlan.get('interfaces', []) + + cfg_pvst = have.get('pvst', None) + if cfg_pvst: + for cfg_vlan in cfg_pvst: + cfg_vlan_id = cfg_vlan.get('vlan_id', None) + cfg_hello_time = cfg_vlan.get('hello_time', None) + cfg_max_age = cfg_vlan.get('max_age', None) + cfg_fwd_delay = cfg_vlan.get('fwd_delay', None) + cfg_bridge_priority = cfg_vlan.get('bridge_priority', None) + cfg_interfaces = cfg_vlan.get('interfaces', []) + + if vlan_id == cfg_vlan_id: + if hello_time and hello_time == cfg_hello_time: + requests.append(self.get_delete_pvst_vlan_cfg_attr(vlan_id, 'hello-time')) + vlans_dict.update({'vlan_id': vlan_id, 'hello_time': hello_time}) + if max_age and max_age == cfg_max_age: + requests.append(self.get_delete_pvst_vlan_cfg_attr(vlan_id, 'max-age')) + vlans_dict.update({'vlan_id': vlan_id, 'max_age': max_age}) + if fwd_delay and fwd_delay == cfg_fwd_delay: + requests.append(self.get_delete_pvst_vlan_cfg_attr(vlan_id, 'forwarding-delay')) + vlans_dict.update({'vlan_id': vlan_id, 'fwd_delay': fwd_delay}) + if bridge_priority and bridge_priority == cfg_bridge_priority: + requests.append(self.get_delete_pvst_vlan_cfg_attr(vlan_id, 'bridge-priority')) + vlans_dict.update({'vlan_id': vlan_id, 'bridge_priority': bridge_priority}) + + if interfaces: + intf_list = [] + for intf in interfaces: + intf_dict = {} + intf_name = intf.get('intf_name', None) + cost = intf.get('cost', None) + port_priority = intf.get('port_priority', None) + + if cfg_interfaces: + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + cfg_cost = cfg_intf.get('cost', None) + cfg_port_priority = cfg_intf.get('port_priority', None) + + if intf_name == cfg_intf_name: + if cost and cost == cfg_cost: + requests.append(self.get_delete_pvst_intf_cfg_attr(vlan_id, intf_name, 'cost')) + intf_dict.update({'intf_name': intf_name, 'cost': cost}) + if port_priority and port_priority == cfg_port_priority: + requests.append(self.get_delete_pvst_intf_cfg_attr(vlan_id, intf_name, 'port-priority')) + intf_dict.update({'intf_name': intf_name, 'port_priority': port_priority}) + if not cost and not port_priority: + requests.append(self.get_delete_pvst_intf(vlan_id, intf_name)) + intf_dict.update({'intf_name': intf_name}) + if intf_dict: + intf_list.append(intf_dict) + if intf_list: + vlans_dict.update({'vlan_id': vlan_id, 'interfaces': intf_list}) + if vlans_dict: + vlans_list.append(vlans_dict) + if vlans_list: + commands['pvst'] = vlans_list + else: + commands.pop('pvst') + + return requests + + def get_delete_stp_rapid_pvst_requests(self, commands, have): + requests = [] + + rapid_pvst = commands.get('rapid_pvst', None) + if rapid_pvst: + vlans_list = [] + for vlan in rapid_pvst: + vlans_dict = {} + vlan_id = vlan.get('vlan_id', None) + hello_time = vlan.get('hello_time', None) + max_age = vlan.get('max_age', None) + fwd_delay = vlan.get('fwd_delay', None) + bridge_priority = vlan.get('bridge_priority', None) + interfaces = vlan.get('interfaces', []) + + cfg_rapid_pvst = have.get('rapid_pvst', None) + if cfg_rapid_pvst: + for cfg_vlan in cfg_rapid_pvst: + cfg_vlan_id = cfg_vlan.get('vlan_id', None) + cfg_hello_time = cfg_vlan.get('hello_time', None) + cfg_max_age = cfg_vlan.get('max_age', None) + cfg_fwd_delay = cfg_vlan.get('fwd_delay', None) + cfg_bridge_priority = cfg_vlan.get('bridge_priority', None) + cfg_interfaces = cfg_vlan.get('interfaces', []) + + if vlan_id == cfg_vlan_id: + if hello_time and hello_time == cfg_hello_time: + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(vlan_id, 'hello-time')) + vlans_dict.update({'vlan_id': vlan_id, 'hello_time': hello_time}) + if max_age and max_age == cfg_max_age: + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(vlan_id, 'max-age')) + vlans_dict.update({'vlan_id': vlan_id, 'max_age': max_age}) + if fwd_delay and fwd_delay == cfg_fwd_delay: + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(vlan_id, 'forwarding-delay')) + vlans_dict.update({'vlan_id': vlan_id, 'fwd_delay': fwd_delay}) + if bridge_priority and bridge_priority == cfg_bridge_priority: + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(vlan_id, 'bridge-priority')) + vlans_dict.update({'vlan_id': vlan_id, 'bridge_priority': bridge_priority}) + + if interfaces: + intf_list = [] + for intf in interfaces: + intf_dict = {} + intf_name = intf.get('intf_name', None) + cost = intf.get('cost', None) + port_priority = intf.get('port_priority', None) + + if cfg_interfaces: + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + cfg_cost = cfg_intf.get('cost', None) + cfg_port_priority = cfg_intf.get('port_priority', None) + + if intf_name == cfg_intf_name: + if cost and cost == cfg_cost: + requests.append(self.get_delete_rapid_pvst_intf_cfg_attr(vlan_id, intf_name, 'cost')) + intf_dict.update({'intf_name': intf_name, 'cost': cost}) + if port_priority and port_priority == cfg_port_priority: + requests.append(self.get_delete_rapid_pvst_intf_cfg_attr(vlan_id, intf_name, 'port-priority')) + intf_dict.update({'intf_name': intf_name, 'port_priority': port_priority}) + if not cost and not port_priority: + requests.append(self.get_delete_rapid_pvst_intf(vlan_id, intf_name)) + intf_dict.update({'intf_name': intf_name}) + if intf_dict: + intf_list.append(intf_dict) + if intf_list: + vlans_dict.update({'vlan_id': vlan_id, 'interfaces': intf_list}) + if vlans_dict: + vlans_list.append(vlans_dict) + if vlans_list: + commands['rapid_pvst'] = vlans_list + else: + commands.pop('rapid_pvst') + + return requests + + def get_delete_all_stp_request(self): + request = {'path': STP_PATH, 'method': DELETE} + + return request + + def get_delete_stp_global_attr(self, attr): + url = '%s/global/config/%s' % (STP_PATH, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_stp_interface(self, intf_name): + url = '%s/interfaces/interface=%s' % (STP_PATH, intf_name) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_stp_interface_attr(self, intf_name, attr): + url = '%s/interfaces/interface=%s/config/%s' % (STP_PATH, intf_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_stp_mstp_cfg_attr(self, attr): + url = '%s/mstp/config/%s' % (STP_PATH, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mst_inst(self, mst_id): + url = '%s/mstp/mst-instances/mst-instance=%s' % (STP_PATH, mst_id) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mst_inst_cfg_attr(self, mst_id, attr): + url = '%s/mstp/mst-instances/mst-instance=%s/config/%s' % (STP_PATH, mst_id, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mst_intf(self, mst_id, intf_name): + url = '%s/mstp/mst-instances/mst-instance=%s/interfaces/interface=%s' % (STP_PATH, mst_id, intf_name) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_mst_intf_cfg_attr(self, mst_id, intf_name, attr): + url = '%s/mstp/mst-instances/mst-instance=%s/interfaces/interface=%s/config/%s' % (STP_PATH, mst_id, intf_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_pvst_vlan_cfg_attr(self, vlan_id, attr): + url = '%s/openconfig-spanning-tree-ext:pvst/vlans=%s/config/%s' % (STP_PATH, vlan_id, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_pvst_intf(self, vlan_id, intf_name): + url = '%s/openconfig-spanning-tree-ext:pvst/vlans=%s/interfaces/interface=%s' % (STP_PATH, vlan_id, intf_name) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_pvst_intf_cfg_attr(self, vlan_id, intf_name, attr): + url = '%s/openconfig-spanning-tree-ext:pvst/vlans=%s/interfaces/interface=%s/config/%s' % (STP_PATH, vlan_id, intf_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_rapid_pvst_vlan_cfg_attr(self, vlan_id, attr): + url = '%s/rapid-pvst/vlan=%s/config/%s' % (STP_PATH, vlan_id, attr) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_rapid_pvst_intf(self, vlan_id, intf_name): + url = '%s/rapid-pvst/vlan=%s/interfaces/interface=%s' % (STP_PATH, vlan_id, intf_name) + request = {'path': url, 'method': DELETE} + + return request + + def get_delete_rapid_pvst_intf_cfg_attr(self, vlan_id, intf_name, attr): + url = '%s/rapid-pvst/vlan=%s/interfaces/interface=%s/config/%s' % (STP_PATH, vlan_id, intf_name, attr) + request = {'path': url, 'method': DELETE} + + return request + + def remove_default_entries(self, data): + stp_global = data.get('global', None) + interfaces = data.get('interfaces', None) + + if stp_global: + loop_guard = stp_global.get('loop_guard', None) + bpdu_filter = stp_global.get('bpdu_filter', None) + portfast = stp_global.get('portfast', None) + hello_time = stp_global.get('hello_time', None) + max_age = stp_global.get('max_age', None) + fwd_delay = stp_global.get('fwd_delay', None) + bridge_priority = stp_global.get('bridge_priority', None) + + if loop_guard is False: + stp_global.pop('loop_guard') + if bpdu_filter is False: + stp_global.pop('bpdu_filter') + if portfast is False: + stp_global.pop('portfast') + if hello_time == 2: + stp_global.pop('hello_time') + if max_age == 20: + stp_global.pop('max_age') + if fwd_delay == 15: + stp_global.pop('fwd_delay') + if bridge_priority == 32768: + stp_global.pop('bridge_priority') + if not stp_global: + data.pop('global') + + if interfaces: + for intf in interfaces: + edge_port = intf.get('edge_port', None) + bpdu_guard = intf.get('bpdu_guard', None) + bpdu_filter = intf.get('bpdu_filter', None) + portfast = intf.get('portfast', None) + uplink_fast = intf.get('uplink_fast', None) + shutdown = intf.get('shutdown', None) + stp_enable = intf.get('stp_enable', None) + + if edge_port is False: + intf.pop('edge_port') + if bpdu_guard is False: + intf.pop('bpdu_guard') + if bpdu_filter is False: + intf.pop('bpdu_filter') + if portfast is False: + intf.pop('portfast') + if uplink_fast is False: + intf.pop('uplink_fast') + if shutdown is False: + intf.pop('shutdown') + if stp_enable: + intf.pop('stp_enable') + + def get_replaced_config(self, want, have): + config_dict = {} + requests = [] + stp_global = want.get('global', None) + new_have = self.remove_default_entries(deepcopy(have)) + new_have = remove_empties(new_have) + cfg_stp_global = new_have.get('global', None) + + if stp_global and cfg_stp_global and stp_global != cfg_stp_global: + requests.append(self.get_delete_all_stp_request()) + return have, requests + + interfaces = want.get('interfaces', None) + cfg_interfaces = have.get('interfaces', None) + if interfaces and cfg_interfaces: + intf_list = [] + for intf in interfaces: + intf_name = intf.get('intf_name', None) + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + if intf_name == cfg_intf_name: + if intf != cfg_intf: + intf_list.append(cfg_intf) + requests.append(self.get_delete_stp_interface(cfg_intf_name)) + if intf_list: + config_dict['interfaces'] = intf_list + + mstp = want.get('mstp', None) + cfg_mstp = have.get('mstp', None) + if mstp and cfg_mstp: + mst_name = mstp.get('mst_name', None) + revision = mstp.get('revision', None) + max_hop = mstp.get('max_hop', None) + hello_time = mstp.get('hello_time', None) + max_age = mstp.get('max_age', None) + fwd_delay = mstp.get('fwd_delay', None) + mst_instances = mstp.get('mst_instances', None) + + cfg_mst_name = cfg_mstp.get('mst_name', None) + cfg_revision = cfg_mstp.get('revision', None) + cfg_max_hop = cfg_mstp.get('max_hop', None) + cfg_hello_time = cfg_mstp.get('hello_time', None) + cfg_max_age = cfg_mstp.get('max_age', None) + cfg_fwd_delay = cfg_mstp.get('fwd_delay', None) + cfg_mst_instances = cfg_mstp.get('mst_instances', None) + + if ((mst_name and mst_name != cfg_mst_name) or (revision and revision != cfg_revision) or (max_hop and max_hop != cfg_max_hop) or + (hello_time and hello_time != cfg_hello_time) or (max_age and max_age != cfg_max_age) or + (fwd_delay and fwd_delay != cfg_fwd_delay)): + config_dict['mstp'] = cfg_mstp + requests.append({'path': '%s/mstp/config' % STP_PATH, 'method': DELETE}) + requests.append({'path': '%s/mstp/mst-instances' % STP_PATH, 'method': DELETE}) + else: + if mst_instances and cfg_mst_instances: + mst_inst_list = [] + for mst in mst_instances: + mst_id = mst.get('mst_id', None) + bridge_priority = mst.get('bridge_priority', None) + vlans = mst.get('vlans', None) + if vlans: + vlans.sort() + interfaces = mst.get('interfaces', None) + for cfg_mst in cfg_mst_instances: + cfg_mst_id = cfg_mst.get('mst_id', None) + cfg_bridge_priority = cfg_mst.get('bridge_priority', None) + cfg_vlans = cfg_mst.get('vlans', None) + if cfg_vlans: + cfg_vlans.sort() + cfg_interfaces = cfg_mst.get('interfaces', None) + + if mst_id == cfg_mst_id: + if ((bridge_priority and bridge_priority != cfg_bridge_priority) or (vlans and vlans != cfg_vlans)): + mst_inst_list.append(cfg_mst) + requests.append(self.get_delete_mst_inst(cfg_mst_id)) + else: + if interfaces and cfg_interfaces: + intf_list = [] + for intf in interfaces: + intf_name = intf.get('intf_name', None) + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + if intf_name == cfg_intf_name: + if intf != cfg_intf: + intf_list.append(cfg_intf) + mst_inst_list.append({'mst_id': cfg_mst_id, 'interfaces': intf_list}) + requests.append(self.get_delete_mst_intf(cfg_mst_id, cfg_intf_name)) + if mst_inst_list: + config_dict['mstp'] = {'mst_instances': mst_inst_list} + + pvst = want.get('pvst', None) + cfg_pvst = have.get('pvst', None) + if pvst and cfg_pvst: + vlans_list, vlans_requests = self.get_replaced_vlans_list(pvst, cfg_pvst, 'pvst') + if vlans_list: + config_dict['pvst'] = vlans_list + requests.extend(vlans_requests) + + rapid_pvst = want.get('rapid_pvst', None) + cfg_rapid_pvst = have.get('rapid_pvst', None) + if rapid_pvst and cfg_rapid_pvst: + vlans_list, vlans_requests = self.get_replaced_vlans_list(rapid_pvst, cfg_rapid_pvst, 'rapid_pvst') + if vlans_list: + config_dict['rapid_pvst'] = vlans_list + requests.extend(vlans_requests) + + return config_dict, requests + + def get_replaced_vlans_list(self, want_data, have_data, protocol): + vlans_list = [] + requests = [] + for vlan in want_data: + vlan_id = vlan.get('vlan_id', None) + hello_time = vlan.get('hello_time', None) + max_age = vlan.get('max_age', None) + fwd_delay = vlan.get('fwd_delay', None) + bridge_priority = vlan.get('bridge_priority', None) + interfaces = vlan.get('interfaces', None) + + for cfg_vlan in have_data: + cfg_vlan_id = cfg_vlan.get('vlan_id', None) + cfg_hello_time = cfg_vlan.get('hello_time', None) + cfg_max_age = cfg_vlan.get('max_age', None) + cfg_fwd_delay = cfg_vlan.get('fwd_delay', None) + cfg_bridge_priority = cfg_vlan.get('bridge_priority', None) + cfg_interfaces = cfg_vlan.get('interfaces', None) + + if vlan_id == cfg_vlan_id: + if ((hello_time and hello_time != cfg_hello_time) or (max_age and max_age != cfg_max_age) or + (fwd_delay and fwd_delay != cfg_fwd_delay) or (bridge_priority and bridge_priority != cfg_bridge_priority)): + vlans_list.append(cfg_vlan) + + if cfg_hello_time: + if protocol == 'pvst': + requests.append(self.get_delete_pvst_vlan_cfg_attr(cfg_vlan_id, 'hello-time')) + elif protocol == 'rapid_pvst': + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(cfg_vlan_id, 'hello-time')) + if cfg_max_age: + if protocol == 'pvst': + requests.append(self.get_delete_pvst_vlan_cfg_attr(cfg_vlan_id, 'max-age')) + elif protocol == 'rapid_pvst': + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(cfg_vlan_id, 'max-age')) + if cfg_fwd_delay: + if protocol == 'pvst': + requests.append(self.get_delete_pvst_vlan_cfg_attr(cfg_vlan_id, 'forwarding-delay')) + elif protocol == 'rapid_pvst': + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(cfg_vlan_id, 'forwarding-delay')) + if cfg_bridge_priority: + if protocol == 'pvst': + requests.append(self.get_delete_pvst_vlan_cfg_attr(cfg_vlan_id, 'bridge-priority')) + elif protocol == 'rapid_pvst': + requests.append(self.get_delete_rapid_pvst_vlan_cfg_attr(cfg_vlan_id, 'bridge-priority')) + if cfg_interfaces: + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + if protocol == 'pvst': + requests.append(self.get_delete_pvst_intf(cfg_vlan_id, cfg_intf_name)) + elif protocol == 'rapid_pvst': + requests.append(self.get_delete_rapid_pvst_intf(cfg_vlan_id, cfg_intf_name)) + + else: + if interfaces and cfg_interfaces: + intf_list = [] + for intf in interfaces: + intf_name = intf.get('intf_name', None) + for cfg_intf in cfg_interfaces: + cfg_intf_name = cfg_intf.get('intf_name', None) + if intf_name == cfg_intf_name: + if intf != cfg_intf: + intf_list.append(cfg_intf) + vlans_list.append({'vlan_id': cfg_vlan_id, 'interfaces': intf_list}) + if protocol == 'pvst': + requests.append(self.get_delete_pvst_intf(cfg_vlan_id, cfg_intf_name)) + elif protocol == 'rapid_pvst': + requests.append(self.get_delete_rapid_pvst_intf(cfg_vlan_id, cfg_intf_name)) + + return vlans_list, requests 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 index 21d575a1f..50225718b 100644 --- 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 @@ -26,17 +26,45 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common i 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, + send_requests, get_diff, ) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( to_request, edit_config ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + get_new_config, + get_formatted_config_diff +) PATCH = 'patch' DELETE = 'delete' +def __derive_system_config_delete_op(key_set, command, exist_conf): + new_conf = exist_conf + + if 'hostname' in command: + new_conf['hostname'] = 'sonic' + if 'interface_naming' in command: + new_conf['interface_naming'] = 'native' + if 'anycast_address' in command and 'anycast_address' in new_conf: + if 'ipv4' in command['anycast_address']: + new_conf['anycast_address']['ipv4'] = True + if 'ipv6' in command['anycast_address']: + new_conf['anycast_address']['ipv6'] = True + if 'mac_address' in command['anycast_address']: + new_conf['anycast_address']['mac_address'] = None + + return True, new_conf + + +TEST_KEYS_formatted_diff = [ + {'__default_ops': {'__delete_op': __derive_system_config_delete_op}}, +] + + class System(ConfigBase): """ The sonic_system class @@ -90,6 +118,17 @@ class System(ConfigBase): if result['changed']: result['after'] = changed_system_facts + new_config = changed_system_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_system_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_system_facts, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -127,6 +166,11 @@ class System(ConfigBase): elif state == 'merged': diff = get_diff(want, have) commands = self._state_merged(want, have, diff) + elif state == 'overridden': + commands = self._state_overridden(want, have) + elif state == 'replaced': + commands = self._state_replaced(want, have) + return commands def _state_merged(self, want, have, diff): @@ -142,6 +186,7 @@ class System(ConfigBase): 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): @@ -167,6 +212,70 @@ class System(ConfigBase): requests = self.get_delete_all_system_request(diff_want) if len(requests) > 0: commands = update_states(diff_want, "deleted") + + return commands, requests + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + new_want = self.patch_want_with_default(want, ac_address_only=True) + replaced_config = self.get_replaced_config(have, new_want) + if replaced_config: + requests = self.get_delete_all_system_request(replaced_config) + send_requests(self._module, requests) + commands = new_want + else: + diff = get_diff(new_want, have) + commands = diff + if not commands: + commands = [] + + requests = [] + + if commands: + requests = self.get_create_system_request(have, commands) + + if len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + new_want = self.patch_want_with_default(want) + if have and have != new_want: + requests = self.get_delete_all_system_request(have) + send_requests(self._module, requests) + have = [] + + commands = [] + requests = [] + + if not have and new_want: + commands = new_want + requests = self.get_create_system_request(have, commands) + if len(requests) > 0: + commands = update_states(commands, "overridden") + else: + commands = [] + return commands, requests def get_create_system_request(self, want, commands): @@ -190,8 +299,9 @@ class System(ConfigBase): return requests def build_create_hostname_payload(self, commands): - payload = {"openconfig-system:config": {}} + payload = {} if "hostname" in commands and commands["hostname"]: + payload = {"openconfig-system:config": {}} payload['openconfig-system:config'].update({"hostname": commands["hostname"]}) return payload @@ -221,6 +331,63 @@ class System(ConfigBase): payload["sonic-sag:SAG_GLOBAL_LIST"].append(temp) return payload + def patch_want_with_default(self, want, ac_address_only=False): + new_want = {} + if want is None: + if ac_address_only: + new_want = {'anycast_address': {'ipv4': True, 'ipv6': True, 'mac_address': None}} + else: + new_want = {'hostname': 'sonic', 'interface_naming': 'native', + 'anycast_address': {'ipv4': True, 'ipv6': True, 'mac_address': None}} + else: + new_want = want.copy() + new_anycast = {} + anycast = want.get('anycast_address', None) + if not anycast: + new_anycast = {'ipv4': True, 'ipv6': True, 'mac_address': None} + else: + new_anycast = anycast.copy() + ipv4 = anycast.get("ipv4", None) + if ipv4 is None: + new_anycast["ipv4"] = True + ipv6 = anycast.get("ipv6", None) + if ipv6 is None: + new_anycast["ipv6"] = True + mac = anycast.get("mac_address", None) + if mac is None: + new_anycast["mac_address"] = None + new_want["anycast_address"] = new_anycast + + if not ac_address_only: + hostname = want.get('hostname', None) + if hostname is None: + new_want["hostname"] = 'sonic' + intf_name = want.get('interface_naming', None) + if intf_name is None: + new_want["interface_naming"] = 'native' + return new_want + + def get_replaced_config(self, have, want): + + replaced_config = dict() + + h_hostname = have.get('hostname', None) + w_hostname = want.get('hostname', None) + if (h_hostname != w_hostname) and w_hostname: + replaced_config = have.copy() + return replaced_config + h_intf_name = have.get('interface_naming', None) + w_intf_name = want.get('interface_naming', None) + if (h_intf_name != w_intf_name) and w_intf_name: + replaced_config = have.copy() + return replaced_config + h_ac_addr = have.get('anycast_address', None) + w_ac_addr = want.get('anycast_address', None) + if (h_ac_addr != w_ac_addr) and w_ac_addr: + replaced_config['anycast_address'] = h_ac_addr + return replaced_config + return replaced_config + def remove_default_entries(self, data): new_data = {} if not data: 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 index 498fcbe28..e376fc82a 100644 --- 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 @@ -27,14 +27,25 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( update_states, get_diff, + get_replaced_config, get_normalize_interface_name, ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + __DELETE_LEAFS_OR_CONFIG_IF_NO_NON_KEY_LEAF, + get_new_config, + get_formatted_config_diff +) PATCH = 'patch' DELETE = 'delete' TEST_KEYS = [ {'host': {'name': ''}}, ] +TEST_KEYS_formatted_diff = [ + {'__default_ops': {'__delete_op': __DELETE_LEAFS_OR_CONFIG_IF_NO_NON_KEY_LEAF}}, + {'host': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] class Tacacs_server(ConfigBase): @@ -91,6 +102,17 @@ class Tacacs_server(ConfigBase): if result['changed']: result['after'] = changed_tacacs_server_facts + new_config = changed_tacacs_server_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_tacacs_server_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_tacacs_server_facts, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -180,6 +202,67 @@ class Tacacs_server(ConfigBase): 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 diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + replaced_config = get_replaced_config(want, have, TEST_KEYS) + + add_commands = [] + if replaced_config: + del_requests = self.get_delete_tacacs_server_requests(replaced_config, have) + requests.extend(del_requests) + commands.extend(update_states(replaced_config, "deleted")) + add_commands = want + else: + add_commands = diff + + if add_commands: + add_requests = self.get_modify_tacacs_server_requests(add_commands, have) + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "replaced")) + + 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 have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + r_diff = get_diff(have, want, TEST_KEYS) + if have and (diff or r_diff): + del_requests = self.get_delete_tacacs_server_requests(have, have) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + want_commands = want + want_requests = self.get_modify_tacacs_server_requests(want_commands, have) + + if len(want_requests) > 0: + requests.extend(want_requests) + commands.extend(update_states(want_commands, "overridden")) + + return commands, requests + def get_tacacs_global_payload(self, conf): payload = {} global_cfg = {} 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 index 73398cf74..9c79cd0e4 100644 --- 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 @@ -26,14 +26,21 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s 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_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) from ansible.module_utils.connection import ConnectionError PATCH = 'patch' DELETE = 'delete' +TEST_KEYS_formatted_diff = [ + {'config': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] class Users(ConfigBase): @@ -83,7 +90,7 @@ class Users(ConfigBase): 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']: + if json_obj and isinstance(json_obj, dict) and 401 == json_obj['code']: auth_error = True warnings.append("Unable to get after configs as password got changed for current user") else: @@ -101,6 +108,19 @@ class Users(ConfigBase): if result['changed']: result['after'] = changed_users_facts + new_config = changed_users_facts + old_config = existing_users_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_users_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + if self._module._diff: + self.sort_lists_in_config(new_config) + self.sort_lists_in_config(old_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -133,8 +153,7 @@ class Users(ConfigBase): 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) + new_diff = get_diff(new_want, have) diff = [] for cfg in new_diff: @@ -187,7 +206,7 @@ class Users(ConfigBase): :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 want is none, then delete all the users except admin if not want: commands = have else: @@ -202,6 +221,65 @@ class Users(ConfigBase): return commands, requests + def _state_replaced(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 replace the current configuration + wit the provided 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, "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 have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + new_want = [{'name': conf['name'], 'role': conf['role']} for conf in want] + new_have = [] + for conf in have: + # Exclude admin user from new_have if it isn't present in new_want + if conf['name'] == 'admin' and not any(cfg['name'] == 'admin' for cfg in new_want): + continue + else: + new_have.append({'name': conf['name'], 'role': conf['role']}) + + if diff or new_want != new_have: + # Delete all users except admin + del_requests = self.get_delete_users_requests(have, have) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + # Merge want configuration + mod_commands = want + mod_requests = self.get_modify_users_requests(mod_commands, have) + + if mod_commands and len(mod_requests) > 0: + requests.extend(mod_requests) + commands.extend(update_states(mod_commands, "overridden")) + + return commands, requests + def get_pwd(self, pw): clear_pwd = hashed_pwd = "" pwd = pw.replace("\\", "") @@ -281,7 +359,7 @@ class Users(ConfigBase): if not commands: return requests - # Skip the asmin user in 'deleted' state. we cannot delete all users + # Skip the admin user in 'deleted' state. we cannot delete all users admin_usr = None for conf in commands: @@ -297,3 +375,7 @@ class Users(ConfigBase): if admin_usr: commands.remove(admin_usr) return requests + + def sort_lists_in_config(self, config): + if config: + config.sort(key=lambda x: x['name']) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlan_mapping/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlan_mapping/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlan_mapping/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlan_mapping/vlan_mapping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlan_mapping/vlan_mapping.py new file mode 100644 index 000000000..acb72db17 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/config/vlan_mapping/vlan_mapping.py @@ -0,0 +1,517 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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_vlan_mapping 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, + remove_empties_from_list, +) +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': {'name': ''}}, + {'mapping': {'service_vlan': '', 'dot1q_tunnel': ''}}, +] + + +class Vlan_mapping(ConfigBase): + """ + The sonic_vlan_mapping class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'vlan_mapping', + ] + + def __init__(self, module): + super(Vlan_mapping, self).__init__(module) + + def get_vlan_mapping_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) + vlan_mapping_facts = facts['ansible_network_resources'].get('vlan_mapping') + if not vlan_mapping_facts: + return [] + return vlan_mapping_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + + existing_vlan_mapping_facts = self.get_vlan_mapping_facts() + commands, requests = self.set_config(existing_vlan_mapping_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_vlan_mapping_facts = self.get_vlan_mapping_facts() + + result['before'] = existing_vlan_mapping_facts + if result['changed']: + result['after'] = changed_vlan_mapping_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_vlan_mapping_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_vlan_mapping_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'] + have = self.convert_vlan_ids_range(have) + want = self.convert_vlan_ids_range(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) + 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 + """ + requests = [] + commands = [] + commands_del = [] + + commands_del = self.get_replaced_delete_list(want, have) + + if commands_del: + commands.extend(update_states(commands_del, "deleted")) + + requests_del = self.get_delete_vlan_mapping_requests(commands_del, have, is_delete_all=True) + if requests_del: + requests.extend(requests_del) + + if diff or commands_del: + requests_rep = self.get_create_vlan_mapping_requests(want, have) + if len(requests_rep): + requests.extend(requests_rep) + commands = update_states(want, "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) + if commands_del: + requests_del = self.get_delete_vlan_mapping_requests(commands_del, have, is_delete_all=True) + requests.extend(requests_del) + commands_del = update_states(commands_del, "deleted") + commands.extend(commands_del) + + commands_over = diff + if diff: + requests_over = self.get_create_vlan_mapping_requests(commands_over, have) + 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 + """ + commands = diff + requests = self.get_create_vlan_mapping_requests(commands, have) + + if commands and len(requests): + 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 = [] + requests = [] + is_delete_all = False + + if not want: + commands = have + is_delete_all = True + else: + commands = want + + requests.extend(self.get_delete_vlan_mapping_requests(commands, have, is_delete_all)) + + if len(requests) == 0: + commands = [] + + if commands: + commands = update_states(commands, 'deleted') + + return commands, requests + + def get_replaced_delete_list(self, commands, have): + matched = [] + + for cmd in commands: + name = cmd.get('name', None) + interface_name = name.replace('/', '%2f') + mapping_list = cmd.get('mapping', []) + + matched_interface_name = None + matched_mapping_list = [] + for existing in have: + have_name = existing.get('name', None) + have_interface_name = have_name.replace('/', '%2f') + have_mapping_list = existing.get('mapping', []) + if interface_name == have_interface_name: + matched_interface_name = have_interface_name + matched_mapping_list = have_mapping_list + + if mapping_list and matched_mapping_list: + returned_mapping_list = [] + for mapping in mapping_list: + service_vlan = mapping.get('service_vlan', None) + + for matched_mapping in matched_mapping_list: + matched_service_vlan = matched_mapping.get('service_vlan', None) + + if matched_service_vlan and service_vlan: + if matched_service_vlan == service_vlan: + priority = mapping.get('priority', None) + have_priority = matched_mapping.get('priority', None) + inner_vlan = mapping.get('inner_vlan', None) + have_inner_vlan = matched_mapping.get('inner_vlan', None) + dot1q_tunnel = mapping.get('dot1q_tunnel', False) + have_dot1q_tunnel = matched_mapping.get('dot1q_tunnel', False) + vlan_ids = mapping.get('vlan_ids', []) + have_vlan_ids = matched_mapping.get('vlan_ids', []) + + if priority != have_priority: + returned_mapping_list.append(mapping) + elif inner_vlan != have_inner_vlan: + returned_mapping_list.append(mapping) + elif dot1q_tunnel != have_dot1q_tunnel: + returned_mapping_list.append(mapping) + elif sorted(vlan_ids) != sorted(have_vlan_ids): + returned_mapping_list.append(mapping) + + if returned_mapping_list: + matched.append({'name': interface_name, 'mapping': returned_mapping_list}) + + return matched + + def get_delete_vlan_mapping_requests(self, commands, have, is_delete_all): + """ Get list of requests to delete vlan mapping configurations + for all interfaces specified by the commands + """ + url = "data/openconfig-interfaces:interfaces/interface={}/openconfig-interfaces-ext:mapped-vlans/mapped-vlan={}" + priority_url = "/ingress-mapping/config/mapped-vlan-priority" + vlan_ids_url = "/match/single-tagged/config/vlan-ids={}" + method = "DELETE" + requests = [] + + # Delete all vlan mappings + if is_delete_all: + for cmd in commands: + name = cmd.get('name', None) + interface_name = name.replace('/', '%2f') + mapping_list = cmd.get('mapping', []) + + if mapping_list: + for mapping in mapping_list: + service_vlan = mapping.get('service_vlan', None) + path = url.format(interface_name, service_vlan) + request = {"path": path, "method": method} + requests.append(request) + + return requests + + else: + for cmd in commands: + name = cmd.get('name', None) + interface_name = name.replace('/', '%2f') + mapping_list = cmd.get('mapping', []) + + # Checks if there is a interface matching the delete command + have_interface_name = None + have_mapping_list = [] + for tmp in have: + tmp_name = tmp.get('name', None) + tmp_interface_name = tmp_name.replace('/', '%2f') + tmp_mapping_list = tmp.get('mapping', []) + if interface_name == tmp_interface_name: + have_interface_name = tmp_interface_name + have_mapping_list = tmp_mapping_list + + # Delete part or all of single mapping + if mapping_list: + for mapping in mapping_list: + service_vlan = mapping.get('service_vlan', None) + vlan_ids = mapping.get('vlan_ids', None) + priority = mapping.get('priority', None) + + # Checks if there is a vlan mapping matching the delete command + have_service_vlan = None + have_vlan_ids = None + have_priority = None + for have_mapping in have_mapping_list: + if have_mapping.get('service_vlan', None) == service_vlan: + have_service_vlan = have_mapping.get('service_vlan', None) + have_vlan_ids = have_mapping.get('vlan_ids', None) + have_priority = have_mapping.get('priority', None) + + if service_vlan and have_service_vlan: + if vlan_ids or priority: + # Delete priority + if priority and have_priority: + path = url.format(interface_name, service_vlan) + priority_url + request = {"path": path, "method": method} + requests.append(request) + # Delete vlan ids + if vlan_ids and have_vlan_ids: + vlan_ids_str = "" + same_vlan_ids_list = self.get_vlan_ids_diff(vlan_ids, have_vlan_ids, same=True) + if same_vlan_ids_list: + for vlan in same_vlan_ids_list: + if vlan_ids_str: + vlan_ids_str = vlan_ids_str + "%2C" + vlan.replace("-", "..") + else: + vlan_ids_str = vlan.replace("-", "..") + path = url.format(interface_name, service_vlan) + vlan_ids_url.format(vlan_ids_str) + request = {"path": path, "method": method} + requests.append(request) + # Delete entire mapping + else: + path = url.format(interface_name, service_vlan) + request = {"path": path, "method": method} + requests.append(request) + # Delete all mappings in an interface + else: + if have_mapping_list: + for mapping in have_mapping_list: + service_vlan = mapping.get('service_vlan', None) + path = url.format(interface_name, service_vlan) + request = {"path": path, "method": method} + requests.append(request) + + return requests + + def get_create_vlan_mapping_requests(self, commands, have): + """ Get list of requests to create/modify vlan mapping configurations + for all interfaces specified by the commands + """ + requests = [] + if not commands: + return requests + + for cmd in commands: + name = cmd.get('name', None) + interface_name = name.replace('/', '%2f') + mapping_list = cmd.get('mapping', []) + + if mapping_list: + for mapping in mapping_list: + requests.append(self.get_create_vlan_mapping_request(interface_name, mapping)) + return requests + + def get_create_vlan_mapping_request(self, interface_name, mapping): + url = "data/openconfig-interfaces:interfaces/interface={}/openconfig-interfaces-ext:mapped-vlans" + body = {} + method = "PATCH" + match_data = None + + service_vlan = mapping.get('service_vlan', None) + priority = mapping.get('priority', None) + vlan_ids = mapping.get('vlan_ids', []) + dot1q_tunnel = mapping.get('dot1q_tunnel', None) + inner_vlan = mapping.get('inner_vlan', None) + + if not dot1q_tunnel: + if len(vlan_ids) > 1: + raise Exception("When dot1q-tunnel is false only one VLAN ID can be passed to the vlan_ids list") + if not vlan_ids and priority: + match_data = None + elif vlan_ids: + if inner_vlan: + match_data = {'double-tagged': {'config': {'inner-vlan-id': inner_vlan, 'outer-vlan-id': int(vlan_ids[0])}}} + else: + match_data = {'single-tagged': {'config': {'vlan-ids': [int(vlan_ids[0])]}}} + if priority: + ing_data = {'config': {'vlan-stack-action': 'SWAP', 'mapped-vlan-priority': priority}} + egr_data = {'config': {'vlan-stack-action': 'SWAP', 'mapped-vlan-priority': priority}} + else: + ing_data = {'config': {'vlan-stack-action': 'SWAP'}} + egr_data = {'config': {'vlan-stack-action': 'SWAP'}} + else: + if inner_vlan: + raise Exception("Inner vlan can only be passed when dot1q_tunnel is false") + if not vlan_ids and priority: + match_data = None + elif vlan_ids: + vlan_ids_list = [] + for vlan in vlan_ids: + vlan_ids_list.append(int(vlan)) + match_data = {'single-tagged': {'config': {'vlan-ids': vlan_ids_list}}} + if priority: + ing_data = {'config': {'vlan-stack-action': 'PUSH', 'mapped-vlan-priority': priority}} + egr_data = {'config': {'vlan-stack-action': 'POP', 'mapped-vlan-priority': priority}} + else: + ing_data = {'config': {'vlan-stack-action': 'PUSH'}} + egr_data = {'config': {'vlan-stack-action': 'POP'}} + if match_data: + body = {'openconfig-interfaces-ext:mapped-vlans': {'mapped-vlan': [ + {'vlan-id': service_vlan, + 'config': {'vlan-id': service_vlan}, + 'match': match_data, + 'ingress-mapping': ing_data, + 'egress-mapping': egr_data} + ]}} + else: + body = {'openconfig-interfaces-ext:mapped-vlans': {'mapped-vlan': [ + {'vlan-id': service_vlan, + 'config': {'vlan-id': service_vlan}, + 'ingress-mapping': ing_data, + 'egress-mapping': egr_data} + ]}} + + request = {"path": url.format(interface_name), "method": method, "data": body} + return request + + def get_vlan_ids_diff(self, vlan_ids, have_vlan_ids, same): + """ Takes two vlan id lists and finds the difference. + :param vlan_ids: list of vlan ids that is looking for diffs + :param have_vlan_ids: list of vlan ids that is being compared to + :param same: if true will instead return list of shared values + :rtype: list(str) + """ + results = [] + + for vlan_id in vlan_ids: + if same: + if vlan_id in have_vlan_ids: + results.append(vlan_id) + else: + if vlan_id not in have_vlan_ids: + results.append(vlan_id) + + return results + + def vlanIdsRangeStr(self, vlanList): + rangeList = [] + for vid in vlanList: + if "-" in vid: + vidList = vid.split("-") + lower = int(vidList[0]) + upper = int(vidList[1]) + for i in range(lower, upper + 1): + rangeList.append(str(i)) + else: + rangeList.append(vid) + return rangeList + + def convert_vlan_ids_range(self, config): + + interface_index = 0 + for conf in config: + name = conf.get('name', None) + interface_name = name.replace('/', '%2f') + mapping_list = conf.get('mapping', []) + + mapping_index = 0 + if mapping_list: + for mapping in mapping_list: + vlan_ids = mapping.get('vlan_ids', None) + + if vlan_ids: + config[interface_index]['mapping'][mapping_index]['vlan_ids'] = self.vlanIdsRangeStr(vlan_ids) + mapping_index = mapping_index + 1 + interface_index = interface_index + 1 + + return config 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 index 404051074..0a0b105a7 100644 --- 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 @@ -14,17 +14,17 @@ 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, + search_obj_in_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, + get_replaced_config, update_states, remove_empties_from_list, ) @@ -35,23 +35,20 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s to_request, edit_config ) -from ansible.module_utils._text import to_native +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) 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': ''}}, ] +TEST_KEYS_formatted_diff = [ + {'config': {'vlan_id': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] class Vlans(ConfigBase): @@ -109,6 +106,18 @@ class Vlans(ConfigBase): if result['changed']: result['after'] = changed_vlans_facts + new_config = changed_vlans_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_vlans_facts, + TEST_KEYS_formatted_diff) + new_config.sort(key=lambda x: x['vlan_id']) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_vlans_facts, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -121,7 +130,7 @@ class Vlans(ConfigBase): to the desired configuration """ want = remove_empties_from_list(self._module.params['config']) - have = existing_vlans_facts + have = remove_empties_from_list(existing_vlans_facts) resp = self.set_state(want, have) return to_list(resp) @@ -157,7 +166,29 @@ class Vlans(ConfigBase): :returns: the commands necessary to migrate the current configuration to the desired configuration """ - return self._state_merged(want, have, diff) + commands = [] + requests = [] + + replaced_config = get_replaced_config(want, have, TEST_KEYS) + replaced_vlans = [] + for config in replaced_config: + vlan_obj = search_obj_in_list(config['vlan_id'], want, 'vlan_id') + if vlan_obj and vlan_obj.get('description', None) is None: + replaced_vlans.append(config) + + if replaced_vlans: + del_requests = self.get_delete_vlans_requests(replaced_vlans, False) + requests.extend(del_requests) + commands.extend(update_states(replaced_config, "deleted")) + + if diff: + rep_commands = diff + rep_requests = self.get_create_vlans_requests(rep_commands) + if len(rep_requests) > 0: + requests.extend(rep_requests) + commands.extend(update_states(rep_commands, "replaced")) + + return commands, requests def _state_overridden(self, want, have, diff): """ The command generator when state is overridden @@ -166,20 +197,41 @@ class Vlans(ConfigBase): :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")) + commands = [] + requests = [] + + r_diff = get_diff(have, want, TEST_KEYS) + if not diff and not r_diff: + return commands, requests + + del_vlans = [] + del_descr_vlans = [] + for config in r_diff: + vlan_obj = search_obj_in_list(config['vlan_id'], want, 'vlan_id') + if vlan_obj: + if vlan_obj.get('description', None) is None: + del_descr_vlans.append(config) + else: + del_vlans.append(config) + + if del_vlans: + del_requests = self.get_delete_vlans_requests(del_vlans, True) + requests.extend(del_requests) + commands.extend(update_states(del_vlans, "deleted")) + + if del_descr_vlans: + del_requests = self.get_delete_vlans_requests(del_descr_vlans, False) + requests.extend(del_requests) + commands.extend(update_states(del_descr_vlans, "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")) + ovr_commands = diff + ovr_requests = self.get_create_vlans_requests(ovr_commands) + if len(ovr_requests) > 0: + requests.extend(ovr_requests) + commands.extend(update_states(ovr_commands, "overridden")) - return commands, ret_requests + return commands, requests def _state_merged(self, want, have, diff): """ The command generator when state is merged @@ -204,16 +256,18 @@ class Vlans(ConfigBase): """ commands = list() # if want is none, then delete all the vlans + delete_vlan = False if not want: commands = have + delete_vlan = True else: # delete specific vlans commands = get_diff(want, diff, TEST_KEYS) - requests = self.get_delete_vlans_requests(commands) + requests = self.get_delete_vlans_requests(commands, delete_vlan) commands = update_states(commands, "deleted") return commands, requests - def get_delete_vlans_requests(self, configs): + def get_delete_vlans_requests(self, configs, delete_vlan=False): requests = [] if not configs: return requests @@ -223,7 +277,7 @@ class Vlans(ConfigBase): for vlan in configs: vlan_id = vlan.get("vlan_id") description = vlan.get("description") - if description: + if description and not delete_vlan: path = self.get_delete_vlan_config_attr(vlan_id, "description") else: path = url.format(vlan_id) 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 index 83deb0ecb..2a07e6456 100644 --- 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 @@ -14,6 +14,7 @@ created from __future__ import absolute_import, division, print_function __metaclass__ = type +from copy import deepcopy from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -30,12 +31,22 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s update_states, normalize_interface_name ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_CONFIG_IF_NO_SUBCONFIG, + get_new_config, + get_formatted_config_diff +) from ansible.module_utils.connection import ConnectionError PATCH = 'patch' DELETE = 'DELETE' +MGMT_VRF_NAME = 'mgmt' TEST_KEYS = [ - {'interfaces': {'name': ''}}, + {'interfaces': {'name': ''}} +] +TEST_KEYS_formatted_diff = [ + {'config': {'name': ''}}, + {'interfaces': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}} ] @@ -97,6 +108,17 @@ class Vrfs(ConfigBase): if result['changed']: result['after'] = changed_vrf_interfaces_facts + new_config = changed_vrf_interfaces_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, existing_vrf_interfaces_facts, + TEST_KEYS_formatted_diff) + result['after(generated)'] = new_config + + if self._module._diff: + result['diff'] = get_formatted_config_diff(existing_vrf_interfaces_facts, + new_config, + self._module._verbosity) result['warnings'] = warnings return result @@ -137,6 +159,10 @@ class Vrfs(ConfigBase): commands, requests = self._state_deleted(want, have) elif state == 'merged': commands, requests = self._state_merged(want, have, diff) + elif state == 'overridden': + commands, requests = self._state_overridden(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have) return commands, requests @@ -172,7 +198,7 @@ class Vrfs(ConfigBase): """ # if want is none, then delete all the vrfs if not want: - commands = have + commands = self.preprocess_mgmt_vrf_for_deleted(have) self.delete_all_flag = True else: commands = want @@ -180,7 +206,7 @@ class Vrfs(ConfigBase): requests = [] if commands: - requests = self.get_delete_vrf_interface_requests(commands, have, want) + requests = self.get_delete_vrf_interface_requests(commands, have) if len(requests) > 0: commands = update_states(commands, "deleted") @@ -189,7 +215,76 @@ class Vrfs(ConfigBase): return commands, requests - def get_delete_vrf_interface_requests(self, configs, have, want): + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + commands = [] + requests = [] + + replaced_config = self.get_replaced_config(have, want) + self.sort_config(replaced_config) + self.sort_config(want) + + if replaced_config and replaced_config != want: + self.delete_all_flag = False + del_requests = self.get_delete_vrf_interface_requests(replaced_config, have, 'replaced') + requests.extend(del_requests) + commands.extend(update_states(replaced_config, "deleted")) + replaced_config = [] + + if not replaced_config and want: + add_commands = want + add_requests = self.get_create_requests(add_commands, have) + + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "replaced")) + + return commands, requests + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :param want: the desired configuration as a dictionary + :param have: the current configuration as a dictionary + :param diff: the difference between want and have + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + self.sort_config(have) + self.sort_config(want) + + commands = [] + requests = [] + + if have and have != want: + want, have = self.preprocess_mgmt_vrf_for_overridden(want, have) + + self.delete_all_flag = True + del_requests = self.get_delete_vrf_interface_requests(have, have) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + add_commands = want + add_requests = self.get_create_requests(add_commands, have) + + if len(add_requests) > 0: + requests.extend(add_requests) + commands.extend(update_states(add_commands, "overridden")) + + return commands, requests + + def get_delete_vrf_interface_requests(self, configs, have, state=None): requests = [] if not configs: return requests @@ -211,21 +306,29 @@ class Vrfs(ConfigBase): continue # if members are not mentioned delet the vrf name - if (self._module.params['state'] == 'deleted' and self.delete_all_flag) or empty_flag: + adjusted_delete_all_flag = name != MGMT_VRF_NAME and self.delete_all_flag + adjusted_empty_flag = empty_flag + if state == 'replaced': + adjusted_empty_flag = empty_flag and name != MGMT_VRF_NAME + + if adjusted_delete_all_flag or adjusted_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) + have_members = matched.get('members', None) + conf_members = conf.get('members', None) + + if have_members: + have_intf = have_members.get('interfaces', None) + conf_intf = conf_members.get('interfaces', None) + if conf_intf: + for del_mem in conf_intf: + if del_mem in have_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 @@ -301,3 +404,62 @@ class Vrfs(ConfigBase): network_inst_payload["openconfig-network-instance:interface"].append(member_payload) return network_inst_payload + + def get_vrf_name(self, vrf): + return vrf.get('name') + + def get_interface_name(self, intf): + return intf.get('name') + + def sort_config(self, conf): + if conf: + conf.sort(key=self.get_vrf_name) + for vrf in conf: + if vrf.get('members', None) and vrf['members'].get('interfaces', None): + vrf['members']['interfaces'].sort(key=self.get_interface_name) + + def get_replaced_config(self, have, want): + + replaced_vrfs = [] + for vrf in want: + vrf_name = vrf['name'] + have_vrf = next((h_vrf for h_vrf in have if h_vrf['name'] == vrf_name), None) + if have_vrf: + replaced_vrfs.append(have_vrf) + + return replaced_vrfs + + def preprocess_mgmt_vrf_for_deleted(self, have): + new_have = have + conf = next((vrf for vrf in new_have if vrf['name'] == MGMT_VRF_NAME), None) + if conf: + new_have = deepcopy(have) + new_have.remove(conf) + return new_have + + def preprocess_mgmt_vrf_for_overridden(self, want, have): + new_want = deepcopy(want) + new_have = deepcopy(have) + h_conf = next((vrf for vrf in new_have if vrf['name'] == MGMT_VRF_NAME), None) + if h_conf: + conf = next((vrf for vrf in new_want if vrf['name'] == MGMT_VRF_NAME), None) + if conf: + mv_intfs = [] + if conf.get('members', None) and conf['members'].get('interfaces', None): + mv_intfs = conf['members'].get('interfaces', []) + + h_mv_intfs = [] + if h_conf.get('members', None) and h_conf['members'].get('interfaces', None): + h_mv_intfs = h_conf['members'].get('interfaces', []) + + mv_intfs.sort(key=lambda x: x['name']) + h_mv_intfs.sort(key=lambda x: x['name']) + if mv_intfs == h_mv_intfs: + new_want.remove(conf) + new_have.remove(h_conf) + elif not h_mv_intfs: + new_have.remove(h_conf) + else: + new_have.remove(h_conf) + + return new_want, new_have 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 index d44adcedf..0a87c98c8 100644 --- 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 @@ -26,7 +26,9 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s ) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( get_diff, - update_states + update_states, + get_replaced_config, + send_requests ) from ansible.module_utils.connection import ConnectionError @@ -124,7 +126,7 @@ class Vxlans(ConfigBase): diff = get_diff(want, have, test_keys) if state == 'overridden': - commands, requests = self._state_overridden(want, have, diff) + commands, requests = self._state_overridden(want, have) elif state == 'deleted': commands, requests = self._state_deleted(want, have, diff) elif state == 'merged': @@ -141,57 +143,63 @@ class Vxlans(ConfigBase): :returns: the commands necessary to migrate the current configuration to the desired configuration """ + requests = [] + replaced_config = get_replaced_config(want, have, test_keys) + + if replaced_config: + self.sort_lists_in_config(replaced_config) + self.sort_lists_in_config(have) + is_delete_all = (replaced_config == have) + if is_delete_all: + requests = self.get_delete_all_vxlan_request(have) + else: + requests = self.get_delete_vxlan_request(replaced_config, have) + + send_requests(self._module, requests) + commands = want + else: + commands = diff 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) + if commands: + requests = self.get_create_vxlans_request(commands, have) + if len(requests) > 0: + commands = update_states(commands, "replaced") + else: + commands = [] + else: + commands = [] return commands, requests - def _state_overridden(self, want, have, diff): + def _state_overridden(self, want, have): """ The command generator when state is overridden :rtype: A list :returns: the commands necessary to migrate the current configuration to the desired configuration """ - requests = [] + self.sort_lists_in_config(want) + self.sort_lists_in_config(have) + + if have and have != want: + requests = self.get_delete_all_vxlan_request(have) + send_requests(self._module, requests) + + have = [] + commands = [] + requests = [] + + if not have and want: + commands = want + requests = self.get_create_vxlans_request(commands, have) - 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) + if len(requests) > 0: + commands = update_states(commands, "overridden") + else: + commands = [] return commands, requests @@ -271,6 +279,7 @@ class Vxlans(ConfigBase): vlan_map_requests = [] src_ip_requests = [] primary_ip_requests = [] + evpn_nvo_requests = [] tunnel_requests = [] # Need to delete in reverse order of creation. @@ -282,6 +291,7 @@ class Vxlans(ConfigBase): vrf_map_list = conf.get('vrf_map', []) src_ip = conf.get('source_ip', None) primary_ip = conf.get('primary_ip', None) + evpn_nvo = conf.get('evpn_nvo', None) if vrf_map_list: vrf_map_requests.extend(self.get_delete_vrf_map_request(conf, conf, name, vrf_map_list)) @@ -291,6 +301,8 @@ class Vxlans(ConfigBase): 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)) + if evpn_nvo: + evpn_nvo_requests.extend(self.get_delete_evpn_request(conf, conf, evpn_nvo)) tunnel_requests.extend(self.get_delete_tunnel_request(conf, conf, name)) if vrf_map_requests: @@ -301,6 +313,8 @@ class Vxlans(ConfigBase): requests.extend(src_ip_requests) if primary_ip_requests: requests.extend(primary_ip_requests) + if evpn_nvo_requests: + requests.extend(evpn_nvo_requests) if tunnel_requests: requests.extend(tunnel_requests) @@ -315,6 +329,7 @@ class Vxlans(ConfigBase): vrf_map_requests = [] vlan_map_requests = [] src_ip_requests = [] + evpn_nvo_requests = [] primary_ip_requests = [] tunnel_requests = [] @@ -325,6 +340,7 @@ class Vxlans(ConfigBase): name = conf['name'] src_ip = conf.get('source_ip', None) + evpn_nvo = conf.get('evpn_nvo', None) primary_ip = conf.get('primary_ip', None) vlan_map_list = conf.get('vlan_map', None) vrf_map_list = conf.get('vrf_map', None) @@ -342,7 +358,7 @@ class Vxlans(ConfigBase): 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): + src_ip is None and evpn_nvo 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", []) @@ -364,7 +380,8 @@ class Vxlans(ConfigBase): 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 evpn_nvo: + evpn_nvo_requests.extend(self.get_delete_evpn_request(conf, matched, evpn_nvo)) if primary_ip: primary_ip_requests.extend(self.get_delete_primary_ip_request(conf, matched, name, primary_ip)) if is_delete_full: @@ -376,6 +393,8 @@ class Vxlans(ConfigBase): requests.extend(vlan_map_requests) if src_ip_requests: requests.extend(src_ip_requests) + if evpn_nvo_requests: + requests.extend(evpn_nvo_requests) if primary_ip_requests: requests.extend(primary_ip_requests) if tunnel_requests: @@ -399,7 +418,7 @@ class Vxlans(ConfigBase): payload = self.build_create_tunnel_payload(conf) request = {"path": url, "method": PATCH, "data": payload} requests.append(request) - if conf.get('source_ip', None): + if conf.get('evpn_nvo', None): requests.append(self.get_create_evpn_request(conf)) return requests @@ -502,12 +521,23 @@ class Vxlans(ConfigBase): payload_url = dict({"sonic-vrf:vni": vrf_map['vni']}) return payload_url - def get_delete_evpn_request(self, conf): + def get_delete_evpn_request(self, conf, matched, del_evpn_nvo): # 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} + requests = [] - return request + url = "data/sonic-vxlan:sonic-vxlan/EVPN_NVO/EVPN_NVO_LIST={evpn_nvo}" + + is_change_needed = False + if matched: + matched_evpn_nvo = matched.get('evpn_nvo', None) + if matched_evpn_nvo and matched_evpn_nvo == del_evpn_nvo: + is_change_needed = True + + if is_change_needed: + request = {"path": url.format(evpn_nvo=conf['evpn_nvo']), "method": DELETE} + requests.append(request) + + return requests def get_delete_tunnel_request(self, conf, matched, name): # Create URL and payload @@ -530,9 +560,7 @@ class Vxlans(ConfigBase): 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) @@ -604,3 +632,18 @@ class Vxlans(ConfigBase): requests.append(request) return requests + + def sort_lists_in_config(self, config): + if config: + config.sort(key=self.get_name) + for cfg in config: + if 'vlan_map' in cfg and cfg['vlan_map']: + cfg['vlan_map'].sort(key=self.get_vni) + if 'vrf_map' in cfg and cfg['vrf_map']: + cfg['vrf_map'].sort(key=self.get_vni) + + def get_name(self, name): + return name.get('name') + + def get_vni(self, vni): + return vni.get('vni') 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 index 5a7bd05c9..541a5805e 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2021 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -13,7 +13,6 @@ 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 ( diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/acl_interfaces/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/acl_interfaces/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/acl_interfaces/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/acl_interfaces/acl_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/acl_interfaces/acl_interfaces.py new file mode 100644 index 000000000..ec2973d84 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/acl_interfaces/acl_interfaces.py @@ -0,0 +1,148 @@ +# +# -*- 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 acl_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 + +from copy import deepcopy + +from ansible.module_utils.connection import ConnectionError +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.acl_interfaces.acl_interfaces import Acl_interfacesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + + +class Acl_interfacesFacts(object): + """ The sonic acl_interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Acl_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 populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for acl_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: + acl_interfaces_configs = self.get_acl_interfaces() + + objs = [] + for interface_config in acl_interfaces_configs.items(): + obj = self.render_config(self.generated_spec, interface_config) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('acl_interfaces', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['acl_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 + """ + config = deepcopy(spec) + config['name'] = conf[0] + config['access_groups'] = [] + + acls = {'mac': [], 'ipv4': [], 'ipv6': []} + for acl in conf[1]: + acl_type = acl.pop('type') + if acl_type in ('ACL_L2', 'openconfig-acl:ACL_L2'): + acls['mac'].append(acl) + elif acl_type in ('ACL_IPV4', 'openconfig-acl:ACL_IPV4'): + acls['ipv4'].append(acl) + elif acl_type in ('ACL_IPV6', 'openconfig-acl:ACL_IPV6'): + acls['ipv6'].append(acl) + + for acl_type, acl_list in acls.items(): + if acl_list: + config['access_groups'].append({ + 'type': acl_type, + 'acls': acl_list + }) + + return config + + def get_acl_interfaces(self): + """Get all interface access-group configurations available in chassis""" + acl_interfaces_path = 'data/openconfig-acl:acl/interfaces' + method = 'GET' + request = [{'path': acl_interfaces_path, '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) + + acl_interfaces = [] + if response[0][1].get('openconfig-acl:interfaces'): + acl_interfaces = response[0][1]['openconfig-acl:interfaces'].get('interface', []) + + acl_interfaces_configs = {} + for interface in acl_interfaces: + acls_list = [] + + ingress_acls = interface.get('ingress-acl-sets', {}).get('ingress-acl-set', []) + for acl in ingress_acls: + if acl.get('config'): + acls_list.append({ + 'name': acl['config']['set-name'], + 'type': acl['config']['type'], + 'direction': 'in' + }) + + egress_acls = interface.get('egress-acl-sets', {}).get('egress-acl-set', []) + for acl in egress_acls: + if acl.get('config'): + acls_list.append({ + 'name': acl['config']['set-name'], + 'type': acl['config']['type'], + 'direction': 'out' + }) + + acl_interfaces_configs[interface['id']] = acls_list + + return acl_interfaces_configs diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bfd/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bfd/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bfd/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bfd/bfd.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bfd/bfd.py new file mode 100644 index 000000000..b8786947d --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bfd/bfd.py @@ -0,0 +1,236 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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 bfd fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.bfd.bfd import BfdArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + + +class BfdFacts(object): + """ The sonic bfd fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = BfdArgs.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 bfd + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + + if not data: + bfd_cfg = self.get_bfd_config(self._module) + data = self.update_bfd(bfd_cfg) + objs = self.render_config(self.generated_spec, data) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['bfd'] = 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 update_bfd(self, data): + bfd_dict = {} + if data: + bfd_dict['profiles'] = self.update_profiles(data) + bfd_dict['single_hops'] = self.update_single_hops(data) + bfd_dict['multi_hops'] = self.update_multi_hops(data) + + return bfd_dict + + def update_profiles(self, data): + all_profiles = [] + bfd_profile = data.get('openconfig-bfd-ext:bfd-profile', None) + if bfd_profile: + profile_list = bfd_profile.get('profile', None) + if profile_list: + for profile in profile_list: + profile_dict = {} + profile_name = profile['profile-name'] + config = profile['config'] + enabled = config.get('enabled', None) + transmit_interval = config.get('desired-minimum-tx-interval', None) + receive_interval = config.get('required-minimum-receive', None) + detect_multiplier = config.get('detection-multiplier', None) + passive_mode = config.get('passive-mode', None) + min_ttl = config.get('minimum-ttl', None) + echo_interval = config.get('desired-minimum-echo-receive', None) + echo_mode = config.get('echo-active', None) + + if profile_name: + profile_dict['profile_name'] = profile_name + if enabled is not None: + profile_dict['enabled'] = enabled + if transmit_interval: + profile_dict['transmit_interval'] = transmit_interval + if receive_interval: + profile_dict['receive_interval'] = receive_interval + if detect_multiplier: + profile_dict['detect_multiplier'] = detect_multiplier + if passive_mode is not None: + profile_dict['passive_mode'] = passive_mode + if min_ttl: + profile_dict['min_ttl'] = min_ttl + if echo_interval: + profile_dict['echo_interval'] = echo_interval + if echo_mode is not None: + profile_dict['echo_mode'] = echo_mode + if profile_dict: + all_profiles.append(profile_dict) + + return all_profiles + + def update_single_hops(self, data): + all_single_hops = [] + bfd_single_hop = data.get('openconfig-bfd-ext:bfd-shop-sessions', None) + if bfd_single_hop: + single_hop_list = bfd_single_hop.get('single-hop', None) + if single_hop_list: + for hop in single_hop_list: + single_hop_dict = {} + remote_address = hop['remote-address'] + vrf = hop['vrf'] + interface = hop['interface'] + local_address = hop['local-address'] + config = hop['config'] + enabled = config.get('enabled', None) + transmit_interval = config.get('desired-minimum-tx-interval', None) + receive_interval = config.get('required-minimum-receive', None) + detect_multiplier = config.get('detection-multiplier', None) + passive_mode = config.get('passive-mode', None) + echo_interval = config.get('desired-minimum-echo-receive', None) + echo_mode = config.get('echo-active', None) + profile_name = config.get('profile-name', None) + + if remote_address: + single_hop_dict['remote_address'] = remote_address + if vrf: + single_hop_dict['vrf'] = vrf + if interface: + single_hop_dict['interface'] = interface + if local_address: + single_hop_dict['local_address'] = local_address + if enabled is not None: + single_hop_dict['enabled'] = enabled + if transmit_interval: + single_hop_dict['transmit_interval'] = transmit_interval + if receive_interval: + single_hop_dict['receive_interval'] = receive_interval + if detect_multiplier: + single_hop_dict['detect_multiplier'] = detect_multiplier + if passive_mode is not None: + single_hop_dict['passive_mode'] = passive_mode + if echo_interval: + single_hop_dict['echo_interval'] = echo_interval + if echo_mode is not None: + single_hop_dict['echo_mode'] = echo_mode + if profile_name: + single_hop_dict['profile_name'] = profile_name + if single_hop_dict: + all_single_hops.append(single_hop_dict) + + return all_single_hops + + def update_multi_hops(self, data): + all_multi_hops = [] + bfd_multi_hop = data.get('openconfig-bfd-ext:bfd-mhop-sessions', None) + if bfd_multi_hop: + multi_hop_list = bfd_multi_hop.get('multi-hop', None) + if multi_hop_list: + for hop in multi_hop_list: + multi_hop_dict = {} + remote_address = hop['remote-address'] + vrf = hop['vrf'] + local_address = hop['local-address'] + config = hop['config'] + enabled = config.get('enabled', None) + transmit_interval = config.get('desired-minimum-tx-interval', None) + receive_interval = config.get('required-minimum-receive', None) + detect_multiplier = config.get('detection-multiplier', None) + passive_mode = config.get('passive-mode', None) + min_ttl = config.get('minimum-ttl', None) + profile_name = config.get('profile-name', None) + + if remote_address: + multi_hop_dict['remote_address'] = remote_address + if vrf: + multi_hop_dict['vrf'] = vrf + if local_address: + multi_hop_dict['local_address'] = local_address + if enabled is not None: + multi_hop_dict['enabled'] = enabled + if transmit_interval: + multi_hop_dict['transmit_interval'] = transmit_interval + if receive_interval: + multi_hop_dict['receive_interval'] = receive_interval + if detect_multiplier: + multi_hop_dict['detect_multiplier'] = detect_multiplier + if passive_mode is not None: + multi_hop_dict['passive_mode'] = passive_mode + if min_ttl: + multi_hop_dict['min_ttl'] = min_ttl + if profile_name: + multi_hop_dict['profile_name'] = profile_name + if multi_hop_dict: + all_multi_hops.append(multi_hop_dict) + + return all_multi_hops + + def get_bfd_config(self, module): + bfd_cfg = None + get_bfd_path = '/data/openconfig-bfd:bfd' + request = {'path': get_bfd_path, 'method': 'get'} + + try: + response = edit_config(module, to_request(module, request)) + if 'openconfig-bfd:bfd' in response[0][1]: + bfd_cfg = response[0][1].get('openconfig-bfd:bfd', None) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + return bfd_cfg 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 index c86b53c2a..7ecf253e7 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( @@ -49,6 +48,7 @@ class BgpFacts(object): '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'], + 'rt_delay': 'route-map-process-delay' } def __init__(self, module, subspec='config', options='options'): @@ -92,8 +92,8 @@ class BgpFacts(object): 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'] + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['bgp'] = remove_empties_from_list(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/bgp_af/bgp_af.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/bgp_af/bgp_af.py index fd37533e4..511fb024a 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2019 Red Hat +# Copyright 2023 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) """ @@ -13,7 +13,6 @@ 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 ( @@ -64,6 +63,10 @@ class Bgp_afFacts(object): 'network': ['network-config', 'network'], 'dampening': ['route-flap-damping', 'config', 'enabled'], 'route_advertise_list': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:route-advertise', 'route-advertise-list'], + 'rd': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'route-distinguisher'], + 'rt_in': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'import-rts'], + 'rt_out': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:config', 'export-rts'], + 'vnis': ['l2vpn-evpn', 'openconfig-bgp-evpn-ext:vnis', 'vni'] } af_redis_params_map = { @@ -104,6 +107,7 @@ class Bgp_afFacts(object): self.update_max_paths(data) self.update_network(data) self.update_route_advertise_list(data) + self.update_vnis(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) @@ -119,8 +123,8 @@ class Bgp_afFacts(object): 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'] + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['bgp_af'] = remove_empties_from_list(params['config']) ansible_facts['ansible_network_resources'].update(facts) return ansible_facts @@ -241,6 +245,38 @@ class Bgp_afFacts(object): rt_adv_lst.append(rt_adv_dict) af['route_advertise_list'] = rt_adv_lst + def update_vnis(self, data): + for conf in data: + afs = conf.get('address_family', []) + if afs: + for af in afs: + vnis = af.get('vnis', None) + if vnis: + vnis_list = [] + for vni in vnis: + vni_dict = {} + vni_config = vni['config'] + vni_number = vni_config.get('vni-number', None) + vni_adv_gw = vni_config.get('advertise-default-gw', None) + vni_adv_svi = vni_config.get('advertise-svi-ip', None) + vni_rd = vni_config.get('route-distinguisher', None) + vni_rt_in = vni_config.get('import-rts', []) + vni_rt_out = vni_config.get('export-rts', []) + if vni_number: + vni_dict['vni_number'] = vni_number + if vni_adv_gw is not None: + vni_dict['advertise_default_gw'] = vni_adv_gw + if vni_adv_svi is not None: + vni_dict['advertise_svi_ip'] = vni_adv_svi + if vni_rd: + vni_dict['rd'] = vni_rd + if vni_rt_in: + vni_dict['rt_in'] = vni_rt_in + if vni_rt_out: + vni_dict['rt_out'] = vni_rt_out + vnis_list.append(vni_dict) + af['vnis'] = vnis_list + def normalize_af_redis_params(self, af): norm_af = list() for e_af in af: 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 index 822db22a4..31cada350 100644 --- 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 @@ -11,7 +11,6 @@ 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 ( @@ -73,8 +72,6 @@ class Bgp_as_pathsFacts(object): 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): 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 index ffa294221..ff4827e61 100644 --- 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 @@ -11,7 +11,6 @@ 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 ( @@ -67,26 +66,37 @@ class Bgp_communitiesFacts(object): 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['name'] = str(name) result['match'] = match + result['members'] = None + result['permit'] = False 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' + if result['type'] == 'expanded': + members = [':'.join(i.split(':')[1:]) for i in members] + members.sort() + result['members'] = {'regex': members} 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} + result['type'] = 'standard' + + if result['type'] == 'standard': + result['local_as'] = None + result['no_advertise'] = None + result['no_export'] = None + result['no_peer'] = None + for i in members: + if "NO_EXPORT_SUBCONFED" in i: + result['local_as'] = True + elif "NO_ADVERTISE" in i: + result['no_advertise'] = True + elif "NO_EXPORT" in i: + result['no_export'] = True + elif "NOPEER" in i: + result['no_peer'] = True + 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): @@ -129,17 +139,5 @@ class Bgp_communitiesFacts(object): :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) + + return conf 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 index b1d7c4ad0..814a25d11 100644 --- 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 @@ -11,7 +11,6 @@ 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 ( @@ -69,34 +68,38 @@ class Bgp_ext_communitiesFacts(object): 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['name'] = str(name) result['match'] = match.lower() - + result['members'] = dict() + result['type'] = 'standard' + result['permit'] = False if permit_str and permit_str == 'PERMIT': result['permit'] = True + if members: + result['type'] = 'expanded' if 'REGEX' in members[0] else 'standard' + if result['type'] == 'expanded': + members = [':'.join(i.split(':')[1:]) for i in members] + members_list = list(map(str, members)) + members_list.sort() + result['members'] = {'regex': members_list} 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 + rt = list() + soo = list() + for member in members: + if member.startswith('route-origin'): + soo.append(':'.join(member.split(':')[1:])) + else: + rt.append(':'.join(member.split(':')[1:])) + route_target_list = list(map(str, rt)) + route_origin_list = list(map(str, soo)) + route_target_list.sort() + route_origin_list.sort() + + if route_target_list and len(route_target_list) > 0: + result['members']['route_target'] = route_target_list + + if route_origin_list and len(route_origin_list) > 0: + result['members']['route_origin'] = route_origin_list bgp_extcommunities_configs.append(result) @@ -142,17 +145,5 @@ class Bgp_ext_communitiesFacts(object): :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) + + return conf 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 index 903b93de1..687420991 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( @@ -110,8 +109,8 @@ class Bgp_neighborsFacts(object): 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'] + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['bgp_neighbors'] = remove_empties_from_list(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/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 index 26119b61c..8f034bb2a 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( @@ -121,7 +120,6 @@ class Bgp_neighbors_afFacts(object): 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']) @@ -142,12 +140,6 @@ class Bgp_neighbors_afFacts(object): 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: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/copp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/copp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/copp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/copp/copp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/copp/copp.py new file mode 100644 index 000000000..52a01d3d1 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/copp/copp.py @@ -0,0 +1,127 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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 copp fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.copp.copp import CoppArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + + +class CoppFacts(object): + """ The sonic copp fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = CoppArgs.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 bfd + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + + if not data: + copp_cfg = self.get_copp_config(self._module) + data = self.update_copp_groups(copp_cfg) + objs = self.render_config(self.generated_spec, data) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['copp'] = 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 update_copp_groups(self, data): + config_dict = {} + all_copp_groups = [] + if data: + copp_groups = data.get('copp-groups', None) + if copp_groups: + copp_group_list = copp_groups.get('copp-group', None) + if copp_group_list: + for group in copp_group_list: + group_dict = {} + copp_name = group['name'] + config = group['config'] + trap_priority = config.get('trap-priority', None) + trap_action = config.get('trap-action', None) + queue = config.get('queue', None) + cir = config.get('cir', None) + cbs = config.get('cbs', None) + + if copp_name: + group_dict['copp_name'] = copp_name + if trap_priority: + group_dict['trap_priority'] = trap_priority + if trap_action: + group_dict['trap_action'] = trap_action + if queue: + group_dict['queue'] = queue + if cir: + group_dict['cir'] = cir + if cbs: + group_dict['cbs'] = cbs + if group_dict: + all_copp_groups.append(group_dict) + if all_copp_groups: + config_dict['copp_groups'] = all_copp_groups + + return config_dict + + def get_copp_config(self, module): + copp_cfg = None + get_copp_path = '/data/openconfig-copp-ext:copp' + request = {'path': get_copp_path, 'method': 'get'} + + try: + response = edit_config(module, to_request(module, request)) + if 'openconfig-copp-ext:copp' in response[0][1]: + copp_cfg = response[0][1].get('openconfig-copp-ext:copp', None) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + return copp_cfg diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_relay/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_relay/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_relay/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_relay/dhcp_relay.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_relay/dhcp_relay.py new file mode 100644 index 000000000..70f78dc24 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_relay/dhcp_relay.py @@ -0,0 +1,208 @@ +# +# -*- 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 dhcp_relay fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.dhcp_relay.dhcp_relay import Dhcp_relayArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible.module_utils.connection import ConnectionError + +SELECT_VALUE_TO_BOOL = { + 'ENABLE': True, + 'DISABLE': False +} + + +class Dhcp_relayFacts(object): + """ The sonic dhcp_relay fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Dhcp_relayArgs.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 dhcp_relay + :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: + dhcp_relay_configs = self.get_dhcp_relay() + dhcpv6_relay_configs = self.get_dhcpv6_relay() + + all_relay_configs = {} + for intf_name, dhcp_relay_config in dhcp_relay_configs.items(): + all_relay_configs[intf_name] = {} + all_relay_configs[intf_name]['ipv4'] = dhcp_relay_config + + for intf_name, dhcpv6_relay_config in dhcpv6_relay_configs.items(): + if all_relay_configs.get(intf_name): + all_relay_configs[intf_name]['ipv6'] = dhcpv6_relay_config + else: + all_relay_configs[intf_name] = {} + all_relay_configs[intf_name]['ipv6'] = dhcpv6_relay_config + + objs = [] + for relay_config in all_relay_configs.items(): + obj = self.render_config(self.generated_spec, relay_config) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('dhcp_relay', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['dhcp_relay'] = utils.remove_empties({'config': params['config']})['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) + config['name'] = conf[0] + + if conf[1].get('ipv4'): + ipv4_dict = conf[1]['ipv4'] + if ipv4_dict.get('policy_action'): + ipv4_dict['policy_action'] = ipv4_dict['policy_action'].lower() + + ipv4_dict['link_select'] = SELECT_VALUE_TO_BOOL.get(ipv4_dict['link_select']) + ipv4_dict['vrf_select'] = SELECT_VALUE_TO_BOOL.get(ipv4_dict['vrf_select']) + + config['ipv4'] = ipv4_dict + else: + config.pop('ipv4') + + if conf[1].get('ipv6'): + ipv6_dict = conf[1]['ipv6'] + ipv6_dict['vrf_select'] = SELECT_VALUE_TO_BOOL.get(ipv6_dict['vrf_select']) + + config['ipv6'] = ipv6_dict + else: + config.pop('ipv6') + + return config + + def get_dhcp_relay(self): + """Get all DHCP relay configurations available in chassis""" + dhcp_relay_interfaces_path = 'data/openconfig-relay-agent:relay-agent/dhcp' + method = 'GET' + request = [{'path': dhcp_relay_interfaces_path, '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) + + dhcp_relay_interfaces = [] + if (response[0][1].get('openconfig-relay-agent:dhcp') + and response[0][1]['openconfig-relay-agent:dhcp'].get('interfaces')): + dhcp_relay_interfaces = response[0][1]['openconfig-relay-agent:dhcp']['interfaces'].get('interface', []) + + dhcp_relay_configs = {} + for interface in dhcp_relay_interfaces: + ipv4_dict = {} + server_addresses = [] + + config = interface.get('config', {}) + for address in config.get('helper-address', []): + temp = {} + temp['address'] = address + server_addresses.append(temp) + ipv4_dict['server_addresses'] = server_addresses + + ipv4_dict['max_hop_count'] = config.get('openconfig-relay-agent-ext:max-hop-count') + ipv4_dict['policy_action'] = config.get('openconfig-relay-agent-ext:policy-action') + ipv4_dict['source_interface'] = config.get('openconfig-relay-agent-ext:src-intf') + ipv4_dict['vrf_name'] = config.get('openconfig-relay-agent-ext:vrf') + + opt_config = interface.get('agent-information-option', {}).get('config', {}) + ipv4_dict['circuit_id'] = opt_config.get('circuit-id') + ipv4_dict['link_select'] = opt_config.get('openconfig-relay-agent-ext:link-select') + ipv4_dict['vrf_select'] = opt_config.get('openconfig-relay-agent-ext:vrf-select') + + dhcp_relay_configs[interface['id']] = ipv4_dict + + return dhcp_relay_configs + + def get_dhcpv6_relay(self): + """Get all DHCPv6 relay configurations available in chassis""" + dhcpv6_relay_interfaces_path = 'data/openconfig-relay-agent:relay-agent/dhcpv6' + method = 'GET' + request = [{'path': dhcpv6_relay_interfaces_path, '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) + + dhcpv6_relay_interfaces = [] + if (response[0][1].get('openconfig-relay-agent:dhcpv6') + and response[0][1]['openconfig-relay-agent:dhcpv6'].get('interfaces')): + dhcpv6_relay_interfaces = response[0][1]['openconfig-relay-agent:dhcpv6']['interfaces'].get('interface', []) + + dhcpv6_relay_configs = {} + for interface in dhcpv6_relay_interfaces: + ipv6_dict = {} + server_addresses = [] + + config = interface.get('config', {}) + for address in config.get('helper-address', []): + temp = {} + temp['address'] = address + server_addresses.append(temp) + ipv6_dict['server_addresses'] = server_addresses + + ipv6_dict['max_hop_count'] = config.get('openconfig-relay-agent-ext:max-hop-count') + ipv6_dict['source_interface'] = config.get('openconfig-relay-agent-ext:src-intf') + ipv6_dict['vrf_name'] = config.get('openconfig-relay-agent-ext:vrf') + + opt_config = interface.get('options', {}).get('config', {}) + ipv6_dict['vrf_select'] = opt_config.get('openconfig-relay-agent-ext:vrf-select') + + dhcpv6_relay_configs[interface['id']] = ipv6_dict + + return dhcpv6_relay_configs diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_snooping/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_snooping/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_snooping/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_snooping/dhcp_snooping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_snooping/dhcp_snooping.py new file mode 100644 index 000000000..c0464c8f1 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/dhcp_snooping/dhcp_snooping.py @@ -0,0 +1,213 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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 dhcp_snooping fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.dhcp_snooping.dhcp_snooping import Dhcp_snoopingArgs +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 Dhcp_snoopingFacts(object): + """ The sonic dhcp_snooping fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Dhcp_snoopingArgs.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 dhcp_snooping + :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: + data = self.get_dhcp_snooping() + + obj = self.render_config(self.generated_spec, data) + + ansible_facts['ansible_network_resources'].pop('dhcp_snooping', None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {'config': obj}) + params_cleaned = {'config': utils.remove_empties(params['config'])} + facts['dhcp_snooping'] = params_cleaned['config'] + + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def get_dhcp_snooping(self): + config = {} + + config['top_level'] = self.get_dhcp_snooping_top_level() + config['binding'] = self.get_dhcp_snooping_binding() + + return config + + def get_dhcp_snooping_top_level(self): + """Get all DHCP snooping configurations available in chassis""" + dhcp_snooping_path = 'data/openconfig-dhcp-snooping:dhcp-snooping' + method = 'GET' + request = [{'path': dhcp_snooping_path, '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) + + config = {} + if (response[0][1].get('openconfig-dhcp-snooping:dhcp-snooping')): + config = response[0][1].get('openconfig-dhcp-snooping:dhcp-snooping') + + return config + + def get_dhcp_snooping_binding(self): + dhcp_binding_snooping_path = 'data/openconfig-dhcp-snooping:dhcp-snooping-binding' + method = 'GET' + request = [{'path': dhcp_binding_snooping_path, '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) + + config = {} + if (response[0][1].get('openconfig-dhcp-snooping:dhcp-snooping-binding')): + config = response[0][1].get('openconfig-dhcp-snooping:dhcp-snooping-binding') + + return config + + 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) + + v4 = {'afi': 'ipv4'} + v6 = {'afi': 'ipv6'} + config['afis'] = [v4, v6] + + # Start with the top-level config from the device. + top_level = conf.get('top_level', {}) + + # Transform the "config" dict from the top-level device config. + deviceConfig = top_level.get('config', {}) + + v4_enabled = deviceConfig.get('dhcpv4-admin-enable', None) + if v4_enabled: + v4['enabled'] = True + else: + v4['enabled'] = False + v6_enabled = deviceConfig.get('dhcpv6-admin-enable', None) + if v6_enabled: + v6['enabled'] = True + else: + v6['enabled'] = False + + v4_verify_mac = deviceConfig.get('dhcpv4-verify-mac-address', None) + if v4_verify_mac is False: + v4['verify_mac'] = False + else: + v4['verify_mac'] = True + v6_verify_mac = deviceConfig.get('dhcpv6-verify-mac-address', None) + if v6_verify_mac is False: + v6['verify_mac'] = False + else: + v6['verify_mac'] = True + + # Transform the "state" dict from the top-level device config. + state = top_level.get('state', {}) + + v4_vlans = state.get('dhcpv4-snooping-vlan', []) + if len(v4_vlans) > 0: + v4['vlans'] = v4_vlans + v6_vlans = state.get('dhcpv6-snooping-vlan', []) + if len(v6_vlans) > 0: + v6['vlans'] = v6_vlans + + STANDARD_ETH = "Eth" + PC = 'PortChannel' + v4_trusted_intf = state.get('dhcpv4-trusted-intf', []) + if len(v4_trusted_intf) > 0: + v4['trusted'] = [] + for intfName in v4_trusted_intf: + intf = {} + if intfName.startswith(STANDARD_ETH) or intfName.startswith(PC): + intf['intf_name'] = intfName + else: + continue + v4['trusted'].append(intf) + v6_trusted_intf = state.get('dhcpv6-trusted-intf', []) + if len(v6_trusted_intf) > 0: + v6['trusted'] = [] + for intfName in v6_trusted_intf: + intf = {} + if intfName.startswith(STANDARD_ETH) or intfName.startswith(PC): + intf['intf_name'] = intfName + else: + continue + v6['trusted'].append(intf) + + # Transform the binding config from the device. + binding = conf.get('binding', {}) + binding_list_container = binding.get('dhcp-snooping-binding-entry-list', {}) + binding_list = binding_list_container.get('dhcp-snooping-binding-list', []) + if len(binding_list) > 0: + v4_entries = [] + v6_entries = [] + for entry in binding_list: + binding = {} + binding['mac_addr'] = entry['mac'] + binding['ip_addr'] = entry['state']['ipaddress'] + binding['intf_name'] = entry['state']['intf'] + binding['vlan_id'] = entry['state']['vlan'] + if entry['iptype'] == 'ipv4': + v4_entries.append(binding) + elif entry['iptype'] == 'ipv6': + v6_entries.append(binding) + if len(v4_entries) > 0: + v4['source_bindings'] = v4_entries + if len(v6_entries) > 0: + v6['source_bindings'] = v6_entries + + return config 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 index 75622632a..dbe597448 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# Copyright 2021 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -32,6 +32,7 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s ) 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.vlan_mapping.vlan_mapping import Vlan_mappingFacts 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 @@ -42,6 +43,21 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s 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 +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.logging.logging import LoggingFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.pki.pki import PkiFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.ip_neighbor.ip_neighbor import Ip_neighborFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.port_group.port_group import Port_groupFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.dhcp_relay.dhcp_relay import Dhcp_relayFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.dhcp_snooping.dhcp_snooping import Dhcp_snoopingFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.acl_interfaces.acl_interfaces import Acl_interfacesFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.l2_acls.l2_acls import L2_aclsFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.l3_acls.l3_acls import L3_aclsFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.lldp_global.lldp_global import Lldp_globalFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.mac.mac import MacFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.bfd.bfd import BfdFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.copp.copp import CoppFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.route_maps.route_maps import Route_mapsFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.stp.stp import StpFacts FACT_LEGACY_SUBSETS = {} FACT_RESOURCE_SUBSETS = dict( @@ -59,6 +75,7 @@ FACT_RESOURCE_SUBSETS = dict( bgp_ext_communities=Bgp_ext_communitiesFacts, mclag=MclagFacts, prefix_lists=Prefix_listsFacts, + vlan_mapping=Vlan_mappingFacts, vrfs=VrfsFacts, vxlans=VxlansFacts, users=UsersFacts, @@ -69,6 +86,21 @@ FACT_RESOURCE_SUBSETS = dict( radius_server=Radius_serverFacts, static_routes=Static_routesFacts, ntp=NtpFacts, + logging=LoggingFacts, + pki=PkiFacts, + ip_neighbor=Ip_neighborFacts, + port_group=Port_groupFacts, + dhcp_relay=Dhcp_relayFacts, + dhcp_snooping=Dhcp_snoopingFacts, + acl_interfaces=Acl_interfacesFacts, + l2_acls=L2_aclsFacts, + l3_acls=L3_aclsFacts, + lldp_global=Lldp_globalFacts, + mac=MacFacts, + bfd=BfdFacts, + copp=CoppFacts, + route_maps=Route_mapsFacts, + stp=StpFacts ) 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 index a36b5d3c0..7ce15fe1b 100644 --- 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 @@ -1,6 +1,6 @@ # # -*- coding: utf-8 -*- -# © Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# © Copyright 2023 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) """ @@ -13,7 +13,6 @@ 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 ( @@ -59,6 +58,7 @@ class InterfacesFacts(object): 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): @@ -94,8 +94,8 @@ class InterfacesFacts(object): if objs: facts['interfaces'] = [] params = utils.validate_config(self.argument_spec, {'config': objs}) - if params: - facts['interfaces'].extend(params['config']) + for cfg in params['config']: + facts['interfaces'].append(utils.remove_empties(cfg)) ansible_facts['ansible_network_resources'].update(facts) return ansible_facts @@ -115,7 +115,7 @@ class InterfacesFacts(object): def transform_config(self, conf): exist_cfg = conf['config'] - trans_cfg = None + trans_cfg = dict() is_loop_back = False name = conf['name'] @@ -125,16 +125,29 @@ class InterfacesFacts(object): 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() + if not (is_loop_back and self.is_loop_back_already_exist(name)) and (name != "eth0") and (name != "Management0"): 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['description'] = exist_cfg.get('description') trans_cfg['mtu'] = exist_cfg['mtu'] if exist_cfg.get('mtu') else 9100 + if name.startswith('Eth') and 'openconfig-if-ethernet:ethernet' in conf: + if conf['openconfig-if-ethernet:ethernet'].get('config', None): + eth_conf = conf['openconfig-if-ethernet:ethernet']['config'] + if 'auto-negotiate' in eth_conf: + trans_cfg['auto_negotiate'] = eth_conf['auto-negotiate'] + trans_cfg['speed'] = eth_conf['port-speed'].split(':', 1)[-1] + if 'openconfig-if-ethernet-ext2:advertised-speed' in eth_conf: + adv_speed_str = eth_conf['openconfig-if-ethernet-ext2:advertised-speed'] + if adv_speed_str != '': + trans_cfg['advertised_speed'] = adv_speed_str.split(",") + trans_cfg['advertised_speed'].sort() + if 'openconfig-if-ethernet-ext2:port-fec' in eth_conf: + trans_cfg['fec'] = eth_conf['openconfig-if-ethernet-ext2:port-fec'].split(':', 1)[-1] + return trans_cfg def reset_loop_backs(self): @@ -143,5 +156,5 @@ class InterfacesFacts(object): def update_loop_backs(self, loop_back): self.loop_backs += "{0},".format(loop_back) - def is_loop_back_already_esist(self, loop_back): + def is_loop_back_already_exist(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/ip_neighbor/ip_neighbor.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ip_neighbor/ip_neighbor.py new file mode 100644 index 000000000..4c077c43f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/ip_neighbor/ip_neighbor.py @@ -0,0 +1,126 @@ +# +# -*- 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 ip_neighbor 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.ip_neighbor.ip_neighbor import Ip_neighborArgs +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 Ip_neighborFacts(object): + """ The sonic ip_neighbor fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Ip_neighborArgs.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 ip_neighbor + :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 neighbor') + # using mock data instead + data = self.get_ip_neighbor_global() + + objs = self.render_config(self.generated_spec, data) + + ansible_facts['ansible_network_resources'].pop('ip_neighbor', None) + + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['ip_neighbor'] = 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_ip_neighbor_global(self): + """Get IP neighbor global configurations""" + + config_path = "data/openconfig-neighbor:neighbor-globals/neighbor-global=Values/config" + config_request = [{"path": config_path, "method": GET}] + config_response = [] + + ip_neigh_glb_conf = dict() + + try: + config_response = edit_config(self._module, to_request(self._module, config_request)) + except ConnectionError as exc: + if re.search("code.*404", str(exc)): + # 'code': 404, 'error-message': 'Resource not found' + return ip_neigh_glb_conf + else: + self._module.fail_json(msg=str(exc), code=exc.code) + + config = dict() + if 'openconfig-neighbor:config' in config_response[0][1]: + config = config_response[0][1].get('openconfig-neighbor:config', {}) + + if "ipv4-arp-timeout" in config: + ip_neigh_glb_conf["ipv4_arp_timeout"] = config["ipv4-arp-timeout"] + + if "ipv4-drop-neighbor-aging-time" in config: + ip_neigh_glb_conf["ipv4_drop_neighbor_aging_time"] = config["ipv4-drop-neighbor-aging-time"] + + if "ipv6-drop-neighbor-aging-time" in config: + ip_neigh_glb_conf["ipv6_drop_neighbor_aging_time"] = config["ipv6-drop-neighbor-aging-time"] + + if "ipv6-nd-cache-expiry" in config: + ip_neigh_glb_conf["ipv6_nd_cache_expiry"] = config["ipv6-nd-cache-expiry"] + + if "num-local-neigh" in config: + ip_neigh_glb_conf["num_local_neigh"] = config["num-local-neigh"] + + return ip_neigh_glb_conf diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_acls/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_acls/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_acls/l2_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_acls/l2_acls.py new file mode 100644 index 000000000..5644cf876 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l2_acls/l2_acls.py @@ -0,0 +1,236 @@ +# +# -*- 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 l2_acls 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.module_utils.connection import ConnectionError +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_acls.l2_acls import L2_aclsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + +ETHERTYPE_FORMAT = '0x{:04x}' + +action_payload_to_value_map = { + 'ACCEPT': 'permit', + 'DISCARD': 'discard', + 'DO_NOT_NAT': 'do-not-nat', + 'DROP': 'deny', + 'TRANSIT': 'transit', +} +ethertype_payload_to_protocol_map = { + '0x0800': 'ipv4', + '0x0806': 'arp', + '0x86dd': 'ipv6', + 'ETHERTYPE_ARP': 'arp', + 'ETHERTYPE_IPV4': 'ipv4', + 'ETHERTYPE_IPV6': 'ipv6' +} +ethertype_payload_to_value_map = { + 'ETHERTYPE_LLDP': '0x88cc', + 'ETHERTYPE_MPLS': '0x8847', + 'ETHERTYPE_ROCE': '0x8915' +} +pcp_value_to_traffic_map = { + 0: 'be', + 1: 'bk', + 2: 'ee', + 3: 'ca', + 4: 'vi', + 5: 'vo', + 6: 'ic', + 7: 'nc' +} + + +class L2_aclsFacts(object): + """ The sonic l2_acls fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = L2_aclsArgs.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 l2_acls + :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: + l2_acls_configs = self.get_l2_acls() + + objs = [] + for l2_acl_config in l2_acls_configs: + obj = self.render_config(self.generated_spec, l2_acl_config) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('l2_acls', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['l2_acls'] = utils.remove_empties({'config': params['config']})['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) + config['name'] = conf['name'] + config['remark'] = conf['remark'] + config['rules'] = conf['rules'] + + for rule in config['rules']: + if ":" in rule['action']: + rule['action'] = rule['action'].split(":")[-1] + rule['action'] = action_payload_to_value_map[rule['action']] + + rule['source'] = {} + rule['destination'] = {} + if rule.get('l2') is None: + rule['source']['any'] = True + rule['destination']['any'] = True + continue + + l2_config = rule.pop('l2') + if l2_config.get('source-mac') and l2_config.get('source-mac-mask'): + if l2_config['source-mac-mask'].lower() == 'ff:ff:ff:ff:ff:ff': + rule['source']['host'] = l2_config['source-mac'].lower() + else: + rule['source']['address'] = l2_config['source-mac'].lower() + rule['source']['address_mask'] = l2_config['source-mac-mask'].lower() + elif l2_config.get('source-mac'): + rule['source']['host'] = l2_config['source-mac'].lower() + else: + rule['source']['any'] = True + + if l2_config.get('destination-mac') and l2_config.get('destination-mac-mask'): + if l2_config['destination-mac-mask'].lower() == 'ff:ff:ff:ff:ff:ff': + rule['destination']['host'] = l2_config['destination-mac'].lower() + else: + rule['destination']['address'] = l2_config['destination-mac'].lower() + rule['destination']['address_mask'] = l2_config['destination-mac-mask'].lower() + elif l2_config.get('destination-mac'): + rule['destination']['host'] = l2_config['destination-mac'].lower() + else: + rule['destination']['any'] = True + + if l2_config.get('ethertype'): + ethertype = l2_config['ethertype'] + rule['ethertype'] = {} + if isinstance(ethertype, str): + ethertype = ethertype.split(':')[-1] + if ethertype in ethertype_payload_to_protocol_map: + rule['ethertype'][ethertype_payload_to_protocol_map[ethertype]] = True + else: + rule['ethertype']['value'] = ethertype_payload_to_value_map[ethertype] + else: + ethertype = ETHERTYPE_FORMAT.format(ethertype) + if ethertype in ethertype_payload_to_protocol_map: + rule['ethertype'][ethertype_payload_to_protocol_map[ethertype]] = True + else: + rule['ethertype']['value'] = ethertype + + if l2_config.get('openconfig-acl-ext:vlanid'): + rule['vlan_id'] = l2_config['openconfig-acl-ext:vlanid'] + if l2_config.get('openconfig-acl-ext:vlan-tag-format') == 'openconfig-acl-ext:MULTI_TAGGED': + rule['vlan_tag_format'] = {'multi_tagged': True} + + if l2_config.get('openconfig-acl-ext:dei') is not None: + rule['dei'] = l2_config['openconfig-acl-ext:dei'] + + if l2_config.get('openconfig-acl-ext:pcp') is not None: + rule['pcp'] = {} + if l2_config.get('openconfig-acl-ext:pcp-mask') is not None: + rule['pcp']['value'] = l2_config['openconfig-acl-ext:pcp'] + rule['pcp']['mask'] = l2_config['openconfig-acl-ext:pcp-mask'] + else: + rule['pcp']['traffic_type'] = pcp_value_to_traffic_map[l2_config['openconfig-acl-ext:pcp']] + + return config + + def get_l2_acls(self): + """Get all l2 acl configurations available in chassis""" + acls_path = 'data/openconfig-acl:acl/acl-sets' + method = 'GET' + request = [{'path': acls_path, '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) + + acls = [] + if response[0][1].get('openconfig-acl:acl-sets'): + acls = response[0][1]['openconfig-acl:acl-sets'].get('acl-set', []) + + l2_acls_configs = [] + for acl in acls: + acl_config = {} + acl_rules = [] + + config = acl['config'] + if config.get('type') not in ('ACL_L2', 'openconfig-acl:ACL_L2'): + continue + + acl_config['name'] = config['name'] + acl_config['remark'] = config.get('description') + acl_config['rules'] = acl_rules + + acl_entries = acl.get('acl-entries', {}).get('acl-entry', []) + for acl_entry in acl_entries: + acl_rule = {} + + acl_entry_config = acl_entry['config'] + acl_rule['sequence_num'] = acl_entry_config['sequence-id'] + acl_rule['remark'] = acl_entry_config.get('description') + + acl_rule['action'] = acl_entry['actions']['config']['forwarding-action'] + acl_rule['l2'] = acl_entry.get('l2', {}).get('config', {}) + + acl_rules.append(acl_rule) + + l2_acls_configs.append(acl_config) + + return l2_acls_configs 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 index 07d7f97dd..78d5b002e 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( @@ -47,8 +46,8 @@ class L2_interfacesFacts(object): self.generated_spec = utils.generate_dict(facts_argument_spec) - def vlan_range_to_list(self, in_range): - range_bounds = in_range.split('-') + def vlan_range_to_list(self, in_range, range_str): + range_bounds = in_range.split(range_str) range_bottom = int(range_bounds[0]) range_top = int(range_bounds[1]) + 1 vlan_list = list(range(range_bottom, range_top)) @@ -79,15 +78,22 @@ class L2_interfacesFacts(object): 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. + # Save trunk vlans and vlan ranges as a list of single vlan dicts: + # Convert single vlan values to strings and convert any ranges + # to the argspec range format. (This block assumes that any string + # value received is a range, using either ".." or "-" as a + # separator between the boundaries of the range. It also assumes + # that any non-string value received is an integer specifying a + # single vlan.) 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)) + vlan_argspec = '' + if isinstance(vlan, str): + vlan_argspec = vlan.replace('"', '') + if '..' in vlan_argspec: + vlan_argspec = vlan_argspec.replace('..', '-') else: - new_det['trunk']['allowed_vlans'].append({'vlan': vlan}) + vlan_argspec = str(vlan) + new_det['trunk']['allowed_vlans'].append({'vlan': vlan_argspec}) l2_interfaces.append(new_det) return l2_interfaces diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_acls/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_acls/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_acls/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_acls/l3_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_acls/l3_acls.py new file mode 100644 index 000000000..799064c9b --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/l3_acls/l3_acls.py @@ -0,0 +1,322 @@ +# +# -*- 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 l3_acls 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.module_utils.connection import ConnectionError +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_acls.l3_acls import L3_aclsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + +IPV4_HOST_MASK = '/32' +IPV6_HOST_MASK = '/128' +L4_PORT_START = 0 +L4_PORT_END = 65535 + +action_payload_to_value_map = { + 'ACCEPT': 'permit', + 'DISCARD': 'discard', + 'DO_NOT_NAT': 'do-not-nat', + 'DROP': 'deny', + 'TRANSIT': 'transit', +} +protocol_payload_to_value_map = { + 'IP_ICMP': 'icmp', + 'IP_IGMP': 2, + 'IP_TCP': 'tcp', + 'IP_UDP': 'udp', + 'IP_RSVP': 46, + 'IP_GRE': 47, + 'IP_AUTH': 51, + 'IP_PIM': 103, + 'IP_L2TP': 115 +} +protocol_number_to_name_map = { + 1: 'icmp', + 6: 'tcp', + 17: 'udp', + 58: 'icmpv6' +} +dscp_value_to_name_map = { + 0: 'default', + 8: 'cs1', + 16: 'cs2', + 24: 'cs3', + 32: 'cs4', + 40: 'cs5', + 48: 'cs6', + 56: 'cs7', + 10: 'af11', + 12: 'af12', + 14: 'af13', + 18: 'af21', + 20: 'af22', + 22: 'af23', + 26: 'af31', + 28: 'af32', + 30: 'af33', + 34: 'af41', + 36: 'af42', + 38: 'af43', + 46: 'ef', + 44: 'voice_admit' +} + + +class L3_aclsFacts(object): + """ The sonic l3_acls fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = L3_aclsArgs.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 l3_acls + :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: + l3_acls_configs = self.get_l3_acls() + + objs = [] + for l3_acl_config in l3_acls_configs: + obj = self.render_config(self.generated_spec, l3_acl_config) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('l3_acls', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['l3_acls'] = utils.remove_empties({'config': params['config']})['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) + config['address_family'] = conf['address_family'] + config['acls'] = conf['acls'] + is_ipv4 = bool(config['address_family'] == 'ipv4') + + for acl in config['acls']: + for rule in acl['rules']: + rule['source'] = {} + rule['destination'] = {} + rule['protocol'] = {} + rule['protocol_options'] = {} + + if ":" in rule['action']: + rule['action'] = rule['action'].split(":")[-1] + rule['action'] = action_payload_to_value_map[rule['action']] + + l2_config = rule.pop('l2', None) + l3_config = rule.pop('l3', None) + l4_config = rule.pop('l4', None) + if l3_config is None: + if is_ipv4: + rule['protocol']['name'] = 'ip' + else: + rule['protocol']['name'] = 'ipv6' + + rule['source']['any'] = True + rule['destination']['any'] = True + continue + + protocol = l3_config.get('protocol') + if protocol is not None: + if isinstance(protocol, str): + protocol = protocol.replace('openconfig-packet-match-types:', '') + protocol = protocol_payload_to_value_map[protocol] + if isinstance(protocol, str): + rule['protocol']['name'] = protocol + else: + rule['protocol']['number'] = protocol + else: + protocol = protocol_number_to_name_map.get(protocol, protocol) + if isinstance(protocol, str): + rule['protocol']['name'] = protocol + else: + rule['protocol']['number'] = protocol + else: + if is_ipv4: + rule['protocol']['name'] = 'ip' + else: + rule['protocol']['name'] = 'ipv6' + + rule['source'] = self._convert_ip_addr_to_spec_fmt(l3_config.get('source-address'), is_ipv4) + rule['destination'] = self._convert_ip_addr_to_spec_fmt(l3_config.get('destination-address'), is_ipv4) + if protocol in ('tcp', 'udp'): + rule['source']['port_number'] = self._convert_l4_port_to_spec_fmt(l4_config.get('source-port')) + rule['destination']['port_number'] = self._convert_l4_port_to_spec_fmt(l4_config.get('destination-port')) + + if protocol in ('icmp', 'icmpv6'): + rule['protocol_options'][protocol] = { + 'code': l4_config.get('openconfig-acl-ext:icmp-code'), + 'type': l4_config.get('openconfig-acl-ext:icmp-type') + } + elif protocol == 'tcp': + rule['protocol_options']['tcp'] = {} + if l4_config.get('openconfig-acl-ext:tcp-session-established'): + rule['protocol_options']['tcp']['established'] = True + else: + for flag in l4_config.get('tcp-flags', []): + flag = flag.split(':')[-1].replace('TCP_', '').lower() + rule['protocol_options']['tcp'][flag] = True + + dscp = l3_config.get('dscp') + if dscp in dscp_value_to_name_map: + rule['dscp'] = {dscp_value_to_name_map[dscp]: True} + else: + rule['dscp'] = {'value': dscp} + + rule['vlan_id'] = l2_config.get('openconfig-acl-ext:vlanid') + + return config + + def get_l3_acls(self): + """Get all l3 acl configurations available in chassis""" + acls_path = 'data/openconfig-acl:acl/acl-sets' + method = 'GET' + request = [{'path': acls_path, '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) + + acls = [] + if response[0][1].get('openconfig-acl:acl-sets'): + acls = response[0][1]['openconfig-acl:acl-sets'].get('acl-set', []) + + ipv4_acls_configs = [] + ipv6_acls_configs = [] + for acl in acls: + is_ipv4 = False + acl_config = {} + acl_rules = [] + + config = acl['config'] + if config.get('type') in ('ACL_IPV4', 'openconfig-acl:ACL_IPV4'): + is_ipv4 = True + elif config.get('type') in ('ACL_IPV6', 'openconfig-acl:ACL_IPV6'): + is_ipv4 = False + else: + continue + + acl_config['name'] = config['name'] + acl_config['remark'] = config.get('description') + acl_config['rules'] = acl_rules + + acl_entries = acl.get('acl-entries', {}).get('acl-entry', []) + for acl_entry in acl_entries: + acl_rule = {} + + acl_entry_config = acl_entry['config'] + acl_rule['sequence_num'] = acl_entry_config['sequence-id'] + acl_rule['remark'] = acl_entry_config.get('description') + + acl_rule['action'] = acl_entry['actions']['config']['forwarding-action'] + acl_rule['l2'] = acl_entry.get('l2', {}).get('config', {}) + if is_ipv4: + acl_rule['l3'] = acl_entry.get('ipv4', {}).get('config', {}) + else: + acl_rule['l3'] = acl_entry.get('ipv6', {}).get('config', {}) + acl_rule['l4'] = acl_entry.get('transport', {}).get('config', {}) + + acl_rules.append(acl_rule) + + if is_ipv4: + ipv4_acls_configs.append(acl_config) + else: + ipv6_acls_configs.append(acl_config) + + l3_acls_configs = [] + if ipv4_acls_configs: + l3_acls_configs.append({'address_family': 'ipv4', 'acls': ipv4_acls_configs}) + if ipv6_acls_configs: + l3_acls_configs.append({'address_family': 'ipv6', 'acls': ipv6_acls_configs}) + + return l3_acls_configs + + @staticmethod + def _convert_ip_addr_to_spec_fmt(ip_addr, is_ipv4=False): + spec_fmt = {} + if ip_addr is not None: + ip_addr = ip_addr.lower() + if is_ipv4: + host_mask = IPV4_HOST_MASK + else: + host_mask = IPV6_HOST_MASK + + if ip_addr.endswith(host_mask): + spec_fmt['host'] = ip_addr.replace(host_mask, '') + else: + spec_fmt['prefix'] = ip_addr + else: + spec_fmt['any'] = True + + return spec_fmt + + @staticmethod + def _convert_l4_port_to_spec_fmt(l4_port): + spec_fmt = {} + if l4_port is not None: + if isinstance(l4_port, str) and '..' in l4_port: + l4_port = [int(i) for i in l4_port.split('..')] + if l4_port[0] == L4_PORT_START: + spec_fmt['lt'] = l4_port[1] + elif l4_port[1] == L4_PORT_END: + spec_fmt['gt'] = l4_port[0] + else: + spec_fmt['range'] = { + 'begin': l4_port[0], + 'end': l4_port[1] + } + else: + spec_fmt['eq'] = int(l4_port) + + return spec_fmt 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 index 69a6dcd44..e91a2b033 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( 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 index 728196813..d83659d92 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lldp_global/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lldp_global/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lldp_global/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lldp_global/lldp_global.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lldp_global/lldp_global.py new file mode 100644 index 000000000..75f3dad51 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/lldp_global/lldp_global.py @@ -0,0 +1,114 @@ +# +# -*- 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 lldp_global fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.lldp_global.lldp_global import Lldp_globalArgs +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 Lldp_globalFacts(object): + """ The sonic lldp_global fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Lldp_globalArgs.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 lldp_global + :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 = self.get_all_lldp_configs() + + ansible_facts['ansible_network_resources'].pop('lldp_global', None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {'config': obj}) + facts['lldp_global'] = utils.remove_empties(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_lldp_configs(self): + """Get all the lldp_global configured in the device""" + request = [{"path": "data/openconfig-lldp:lldp/config", "method": GET}] + lldp_global_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) + lldp_global_data['tlv_select'] = {} + lldp_global_data['tlv_select']['management_address'] = True + lldp_global_data['tlv_select']['system_capabilities'] = True + lldp_global_data['enable'] = True + if 'openconfig-lldp:config' in response[0][1]: + raw_lldp_global_data = response[0][1]['openconfig-lldp:config'] + if 'enabled' in raw_lldp_global_data: + lldp_global_data['enable'] = raw_lldp_global_data['enabled'] + if 'hello-timer' in raw_lldp_global_data: + lldp_global_data['hello_time'] = raw_lldp_global_data['hello-timer'] + if 'openconfig-lldp-ext:mode' in raw_lldp_global_data: + lldp_global_data['mode'] = raw_lldp_global_data['openconfig-lldp-ext:mode'].lower() + if 'system-description' in raw_lldp_global_data: + lldp_global_data['system_description'] = raw_lldp_global_data['system-description'] + if 'system-name' in raw_lldp_global_data: + lldp_global_data['system_name'] = raw_lldp_global_data['system-name'] + if 'openconfig-lldp-ext:multiplier' in raw_lldp_global_data: + lldp_global_data['multiplier'] = raw_lldp_global_data['openconfig-lldp-ext:multiplier'] + if 'suppress-tlv-advertisement' in raw_lldp_global_data: + for tlv_select in raw_lldp_global_data['suppress-tlv-advertisement']: + tlv_select = tlv_select.replace('openconfig-lldp-types:', '').lower() + if tlv_select in ('management_address', 'system_capabilities'): + lldp_global_data['tlv_select'][tlv_select] = False + return lldp_global_data diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/logging/logging.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/logging/logging.py new file mode 100644 index 000000000..c3c05035e --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/logging/logging.py @@ -0,0 +1,128 @@ +# +# -*- 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 logging fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.logging.logging import LoggingArgs +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 LoggingFacts(object): + """ The sonic logging fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = LoggingArgs.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 logging + :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_logging_configuration() + + obj = self.render_config(self.generated_spec, data) + + ansible_facts['ansible_network_resources'].pop('logging', None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {'config': obj}) + facts['logging'] = 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_logging_configuration(self): + """Get all logging configuration""" + + config_request = [{"path": "data/openconfig-system:system/logging", "method": GET}] + config_response = [] + try: + config_response = edit_config(self._module, to_request(self._module, config_request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + logging_response = dict() + if 'openconfig-system:logging' in config_response[0][1]: + logging_response = config_response[0][1].get('openconfig-system:logging', {}) + + remote_servers = [] + if 'remote-servers' in logging_response: + remote_servers = logging_response['remote-servers'].get('remote-server', []) + + logging_config = dict() + + logging_servers = [] + for remote_server in remote_servers: + rs_config = remote_server.get('config', {}) + logging_server = {} + logging_server['host'] = rs_config['host'] + if 'openconfig-system-ext:message-type' in rs_config: + logging_server['message_type'] = rs_config['openconfig-system-ext:message-type'] + if 'openconfig-system-ext:source-interface' in rs_config: + logging_server['source_interface'] = rs_config['openconfig-system-ext:source-interface'] + if logging_server['source_interface'].startswith("Management") or \ + logging_server['source_interface'].startswith("Mgmt"): + logging_server['source_interface'] = 'eth0' + if 'openconfig-system-ext:vrf-name' in rs_config: + logging_server['vrf'] = rs_config['openconfig-system-ext:vrf-name'] + if 'remote-port' in rs_config: + logging_server['remote_port'] = rs_config['remote-port'] + + logging_servers.append(logging_server) + + logging_config['remote_servers'] = logging_servers + + return logging_config diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mac/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mac/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mac/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mac/mac.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mac/mac.py new file mode 100644 index 000000000..26f705040 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/mac/mac.py @@ -0,0 +1,151 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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 mac_address 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.mac.mac import MacArgs +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' + + +class MacFacts(object): + """ The sonic mac fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = MacArgs.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 mac_address + :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: + data = self.update_mac(self._module) + # operate on a collection of resource x + for conf in data: + if conf: + obj = self.render_config(conf) + # split the config into instances of the resource + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('mac', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': remove_empties_from_list(objs)}) + facts['mac'] = params['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def render_config(self, 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 update_mac(self, module): + mac_address_cfg_list = [] + vrfs = get_all_vrfs(module) + for vrf_name in vrfs: + aging_time = self.get_config(vrf_name, module, 'fdb/config/mac-aging-time', 'openconfig-network-instance:mac-aging-time') + dampening_cfg_dict = self.get_config(vrf_name, module, 'openconfig-mac-dampening:mac-dampening/config', 'openconfig-mac-dampening:config') + entries_dict = self.get_config(vrf_name, module, 'fdb/mac-table/entries', 'openconfig-network-instance:entries') + cfg_dict = {} + mac_dict = {} + mac_table_entries = [] + dampening_interval = dampening_cfg_dict.get('interval', None) + dampening_threshold = dampening_cfg_dict.get('threshold', None) + + if entries_dict: + entry_list = entries_dict.get('entry', []) + for entry in entry_list: + entry_dict = {} + mac_address = entry.get('mac-address', None) + vlan_id = entry.get('vlan', None) + interface = entry.get('interface', {}).get('interface-ref', {}).get('config', {}).get('interface', None) + if mac_address: + entry_dict['mac_address'] = mac_address + if vlan_id: + entry_dict['vlan_id'] = vlan_id + if interface: + entry_dict['interface'] = interface + if entry_dict: + mac_table_entries.append(entry_dict) + + if aging_time: + mac_dict['aging_time'] = aging_time + if dampening_interval: + mac_dict['dampening_interval'] = dampening_interval + if dampening_threshold: + mac_dict['dampening_threshold'] = dampening_threshold + if mac_table_entries: + mac_dict['mac_table_entries'] = mac_table_entries + if mac_dict: + cfg_dict['mac'] = mac_dict + cfg_dict['vrf_name'] = vrf_name + mac_address_cfg_list.append(cfg_dict) + + return mac_address_cfg_list + + def get_config(self, vrf_name, module, path, name): + cfg_dict = {} + get_path = '%s=%s/%s' % (NETWORK_INSTANCE_PATH, vrf_name, path) + request = {'path': get_path, 'method': 'get'} + + try: + response = edit_config(module, to_request(module, request)) + if name in response[0][1]: + cfg_dict = response[0][1].get(name, None) + except Exception as exc: + pass + + return cfg_dict 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 index 69864cdf9..9c57f6cc0 100644 --- 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 @@ -23,6 +23,9 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s to_request, edit_config ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + get_ranges_in_list +) from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.mclag.mclag import MclagArgs from ansible.module_utils.connection import ConnectionError @@ -76,7 +79,7 @@ class MclagFacts(object): facts = {} if objs: params = utils.validate_config(self.argument_spec, {'config': objs}) - facts['mclag'] = params['config'] + facts['mclag'] = utils.remove_empties(params['config']) ansible_facts['ansible_network_resources'].update(facts) return ansible_facts @@ -118,6 +121,8 @@ class MclagFacts(object): config['peer_link'] = domain_config['peer-link'] if domain_config.get('mclag-system-mac', None): config['system_mac'] = domain_config['mclag-system-mac'] + if domain_config.get('delay-restore', None): + config['delay_restore'] = domain_config['delay-restore'] if conf.get('vlan-interfaces', None) and conf['vlan-interfaces'].get('vlan-interface', None): vlans_list = [] @@ -125,7 +130,15 @@ class MclagFacts(object): for vlan in vlan_data: vlans_list.append({'vlan': vlan['name']}) if vlans_list: - config['unique_ip'] = {'vlans': vlans_list} + config['unique_ip'] = {'vlans': self.get_vlan_range_list(vlans_list)} + + if conf.get('vlan-ifs', None) and conf['vlan-ifs'].get('vlan-if', None): + vlans_list = [] + vlan_data = conf['vlan-ifs']['vlan-if'] + for vlan in vlan_data: + vlans_list.append({'vlan': vlan['name']}) + if vlans_list: + config['peer_gateway'] = {'vlans': self.get_vlan_range_list(vlans_list)} if conf.get('interfaces', None) and conf['interfaces'].get('interface', None): portchannels_list = [] @@ -136,4 +149,27 @@ class MclagFacts(object): if portchannels_list: config['members'] = {'portchannels': portchannels_list} + if conf.get('mclag-gateway-macs', None) and conf['mclag-gateway-macs'].get('mclag-gateway-mac', None): + gw_mac_data = conf['mclag-gateway-macs']['mclag-gateway-mac'] + if gw_mac_data[0].get('config', None) and gw_mac_data[0]['config'].get('gateway-mac', None): + config['gateway_mac'] = gw_mac_data[0]['config']['gateway-mac'] + return config + + @staticmethod + def get_vlan_range_list(vlans_list): + """Returns list of VLAN ranges for given list of VLANs""" + vlan_range_list = [] + vlan_id_list = [] + + for vlan in vlans_list: + match = re.match(r'Vlan(\d+)', vlan['vlan']) + if match: + vlan_id_list.append(int(match.group(1))) + + if vlan_id_list: + vlan_id_list.sort() + for vlan_range in get_ranges_in_list(vlan_id_list): + vlan_range_list.append({'vlan': 'Vlan{0}'.format('-'.join(map(str, (vlan_range[0], vlan_range[-1])[:len(vlan_range)])))}) + + return vlan_range_list 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 index a47142b47..d52516705 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( @@ -113,16 +112,16 @@ class NtpFacts(object): ntp_config = dict() - if 'network-instance' in ntp_global_config: + if 'network-instance' in ntp_global_config and ntp_global_config['network-instance']: 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: + if 'source-interface' in ntp_global_config and ntp_global_config['source-interface']: ntp_config['source_interfaces'] = ntp_global_config['source-interface'] - if 'trusted-key' in ntp_global_config: + if 'trusted-key' in ntp_global_config and ntp_global_config['trusted-key']: ntp_config['trusted_keys'] = ntp_global_config['trusted-key'] servers = [] @@ -134,8 +133,10 @@ class NtpFacts(object): server['key_id'] = ntp_server['config']['key-id'] server['minpoll'] = ntp_server['config'].get('minpoll', None) server['maxpoll'] = ntp_server['config'].get('maxpoll', None) + server['prefer'] = ntp_server['config'].get('prefer', None) servers.append(server) - ntp_config['servers'] = servers + if servers: + ntp_config['servers'] = servers keys = [] for ntp_key in ntp_keys: @@ -148,6 +149,7 @@ class NtpFacts(object): key['key_type'] = key_type key['key_value'] = ntp_key['config'].get('key-value', None) keys.append(key) - ntp_config['ntp_keys'] = keys + if keys: + ntp_config['ntp_keys'] = keys return ntp_config diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/pki/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/pki/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/pki/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/pki/pki.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/pki/pki.py new file mode 100644 index 000000000..240c50335 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/pki/pki.py @@ -0,0 +1,144 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2022 Dell EMC +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic pki fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.pki.pki import ( + PkiArgs, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config, +) + +pki_path = "data/openconfig-pki:pki/" +security_profiles_path = "data/openconfig-pki:pki/security-profiles" + + +class PkiFacts(object): + """The sonic pki fact class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = PkiArgs.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 pki + :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 + resources = {} + if not data: + result = self.get_pki() + if len(result) > 0 and result[0]: + code, resources = result[0] + + objs = {} + if ( + resources.get("openconfig-pki:pki") + and resources.get("openconfig-pki:pki").get("security-profiles") + and resources.get("openconfig-pki:pki") + .get("security-profiles") + .get("security-profile") + ): + sps = ( + resources.get("openconfig-pki:pki") + .get("security-profiles") + .get("security-profile") + ) + sps_conf = [r.get("config") for r in sps] + rep_conf = [] + for c in sps_conf: + conf = {} + for k, v in c.items(): + conf[k.replace("-", "_")] = v + rep_conf.append(conf) + objs["security_profiles"] = rep_conf + if ( + resources.get("openconfig-pki:pki") + and resources.get("openconfig-pki:pki").get("trust-stores") + and resources.get("openconfig-pki:pki") + .get("trust-stores") + .get("trust-store") + ): + tsts = ( + resources.get("openconfig-pki:pki") + .get("trust-stores") + .get("trust-store") + ) + tsts_conf = [r.get("config") for r in tsts] + rep_conf = [] + for c in tsts_conf: + conf = {} + for k, v in c.items(): + conf[k.replace("-", "_")] = v + rep_conf.append(conf) + + objs["trust_stores"] = rep_conf + + ansible_facts["ansible_network_resources"].pop("pki", None) + facts = {} + if objs: + params = utils.validate_config( + self.argument_spec, {"config": objs} + ) + facts["pki"] = params["config"] + + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts + + def get_pki(self): + request = {"path": pki_path, "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) + + return response + + 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) + return utils.remove_empties(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 index 938bd6423..08b143dd5 100644 --- 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 @@ -11,8 +11,6 @@ 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 ( @@ -29,7 +27,6 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s from ansible.module_utils.connection import ConnectionError GET = "get" -POST = "post" class Port_breakoutFacts(object): @@ -98,8 +95,8 @@ class Port_breakoutFacts(object): 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}] + """Get all the port_breakout configured on the device""" + request = [{"path": "data/sonic-port-breakout:sonic-port-breakout/BREAKOUT_CFG/BREAKOUT_CFG_LIST", "method": GET}] port_breakout_list = [] try: response = edit_config(self._module, to_request(self._module, request)) @@ -107,12 +104,12 @@ class Port_breakoutFacts(object): 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', []) + if "sonic-port-breakout:BREAKOUT_CFG_LIST" in response[0][1]: + raw_port_breakout_list = response[0][1].get("sonic-port-breakout:BREAKOUT_CFG_LIST", []) for port_breakout in raw_port_breakout_list: name = port_breakout.get('port', None) - mode = port_breakout.get('defmode', None) + mode = port_breakout.get('brkout_mode', None) if name and mode: if '[' in mode: mode = mode[:mode.index('[')] diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/port_group/port_group.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/port_group/port_group.py new file mode 100644 index 000000000..c6e4816c4 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/port_group/port_group.py @@ -0,0 +1,116 @@ +# +# -*- 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 port group fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.port_group.port_group import Port_groupArgs +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 Port_groupFacts(object): + """ The sonic port group fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Port_groupArgs.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 groups + :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 port-group') + # using mock data instead + data = self.get_port_groups() + + objs = [] + 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_group', None) + facts = {} + if objs: + facts['port_group'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + if params: + facts['port_group'].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_port_groups(self): + """Get all the port group configurations""" + + pgs_request = [{"path": "data/openconfig-port-group:port-groups/port-group", "method": GET}] + try: + pgs_response = edit_config(self._module, to_request(self._module, pgs_request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + + pgs_config = [] + if "openconfig-port-group:port-group" in pgs_response[0][1]: + pgs_config = pgs_response[0][1].get("openconfig-port-group:port-group", []) + + pgs = [] + for pg_config in pgs_config: + pg = dict() + if 'config' in pg_config: + pg['id'] = pg_config['id'] + speed_str = pg_config['config'].get('speed', None) + if speed_str: + pg['speed'] = speed_str.split(":", 1)[-1] + pgs.append(pg) + + return pgs 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 index 72593b225..33ab55a72 100644 --- 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 @@ -11,8 +11,6 @@ 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 ( @@ -103,7 +101,7 @@ class Radius_serverFacts(object): 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: + if 'secret-key' in raw_radius_global_data and raw_radius_global_data['secret-key']: 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'] diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/route_maps/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/route_maps/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/route_maps/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/route_maps/route_maps.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/route_maps/route_maps.py new file mode 100644 index 000000000..05e6d6188 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/route_maps/route_maps.py @@ -0,0 +1,517 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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 route_maps 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.route_maps.route_maps import Route_mapsArgs + +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.sonic \ + import to_request, edit_config + + +class Route_mapsFacts(object): + """ The sonic route_maps fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Route_mapsArgs.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 route_maps + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + # Fetch data from the current device configuration + # (Skip if operating on previously fetched configuration.) + data = self.get_all_route_maps() + + # split the unparsed route map configuration list into a list + # of parsed route map statement "instances" (dictonary "objects"). + route_maps = [] + for route_map_cfg in data: + route_map_stmts = self.route_map_cfg_parse(route_map_cfg) + if route_map_stmts: + route_maps.extend(route_map_stmts) + + ansible_facts['ansible_network_resources'].pop('route_maps', None) + facts = {} + if route_maps: + params = utils.validate_config(self.argument_spec, + {'config': route_maps}) + params_cleaned = {'config': remove_empties_from_list(params['config'])} + facts['route_maps'] = params_cleaned['config'] + ansible_facts['ansible_network_resources'].update(facts) + return ansible_facts + + def get_all_route_maps(self): + '''Execute a REST "GET" API to fetch all of the current route map configuration + from the target device.''' + + route_map_fetch_spec = \ + "openconfig-routing-policy:routing-policy/policy-definitions" + route_map_resp_key = "openconfig-routing-policy:policy-definitions" + route_map_key = "policy-definition" + url = "data/%s" % route_map_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)) + + route_maps_unparsed = [] + resp_route_map_envelope = response[0][1].get(route_map_resp_key, None) + if resp_route_map_envelope: + route_maps_unparsed = resp_route_map_envelope.get(route_map_key, None) + return route_maps_unparsed + + def route_map_cfg_parse(self, unparsed_route_map): + '''Parse the raw input configuration JSON representation for the route map specified + by the "unparsed_route_map" input parameter. Parse the information to + convert it to a dictionary matching the "argspec" for the "route_maps" resource + module.''' + + parsed_route_map_stmts = [] + + if not unparsed_route_map.get("config"): + return parsed_route_map_stmts + route_map_name = unparsed_route_map.get('name') + if not route_map_name: + return parsed_route_map_stmts + route_map_statements = unparsed_route_map.get('statements') + if not route_map_statements: + return parsed_route_map_stmts + route_map_stmts_list = route_map_statements.get('statement') + if not route_map_stmts_list: + return parsed_route_map_stmts + + for route_map_stmt in route_map_stmts_list: + parsed_route_map_stmt = {} + parsed_seq_num = route_map_stmt.get('name') + if not parsed_seq_num: + continue + parsed_route_map_stmt['map_name'] = route_map_name + parsed_route_map_stmt['sequence_num'] = parsed_seq_num + self.get_route_map_stmt_set_attr(route_map_stmt, parsed_route_map_stmt) + self.get_route_map_stmt_match_attr(route_map_stmt, parsed_route_map_stmt) + self.get_route_map_call_attr(route_map_stmt, parsed_route_map_stmt) + parsed_route_map_stmts.append(parsed_route_map_stmt) + + return parsed_route_map_stmts + + def get_route_map_stmt_set_attr(self, route_map_stmt, parsed_route_map_stmt): + '''Parse the "set" attribute portion of the raw input configuration JSON + representation for the route map "statement" specified + by the "route_map_stmt," input parameter. Parse the information to + convert it to a dictionary matching the "argspec" for the "route_maps" resource + module.''' + + stmt_actions = route_map_stmt.get('actions') + if not stmt_actions: + return + + # Fetch the permit/deny action for the route map statement + actions_config = stmt_actions.get('config') + if not actions_config: + return + permit_deny_config = actions_config.get('policy-result') + if not permit_deny_config: + return + if permit_deny_config == "ACCEPT_ROUTE": + parsed_route_map_stmt['action'] = "permit" + elif permit_deny_config == "REJECT_ROUTE": + parsed_route_map_stmt['action'] = "deny" + else: + return + + # Create a dict object to hold "set" attributes. + parsed_route_map_stmt['set'] = {} + parsed_route_map_stmt_set = parsed_route_map_stmt['set'] + + # Fetch non-required top level set attributes + set_metric_action = stmt_actions.get('metric-action') + if set_metric_action: + set_metric_action_cfg = set_metric_action.get('config') + if set_metric_action_cfg: + metric_action = set_metric_action_cfg.get('action') + if metric_action: + parsed_route_map_stmt_set['metric'] = {} + if metric_action == 'openconfig-routing-policy:METRIC_SET_VALUE': + value = set_metric_action_cfg.get('metric') + if value: + parsed_route_map_stmt_set['metric']['value'] = value + elif metric_action == 'openconfig-routing-policy:METRIC_SET_RTT': + parsed_route_map_stmt_set['metric']['rtt_action'] = 'set' + elif metric_action == 'openconfig-routing-policy:METRIC_ADD_RTT': + parsed_route_map_stmt_set['metric']['rtt_action'] = 'add' + elif metric_action == 'openconfig-routing-policy:METRIC_SUBTRACT_RTT': + parsed_route_map_stmt_set['metric']['rtt_action'] = 'subtract' + + # Possible anomalous state due to partial deletion of metric config via REST + if parsed_route_map_stmt_set['metric'] == {}: + parsed_route_map_stmt_set.pop('metric') + + # Fetch BGP policy action attributes + set_bgp_policy = stmt_actions.get('openconfig-bgp-policy:bgp-actions') + if set_bgp_policy: + self.get_route_map_set_bgp_policy_attr(set_bgp_policy, parsed_route_map_stmt_set) + + def get_route_map_set_bgp_policy_attr(self, set_bgp_policy, parsed_route_map_stmt_set): + '''Parse the BGP policy "set" attribute portion of the raw input + configuration JSON representation within the route map "statement" + that is currently being parsed. The configuration section to be parsed + is specified by the "set_bgp_policy" input parameter. Parse the + information to convert it to a dictionary matching the "argspec" for + the "route_maps" resource module.''' + + # Fetch as_path_prepend config + set_as_path_top = set_bgp_policy.get('set-as-path-prepend') + if set_as_path_top and set_as_path_top.get('config'): + as_path_prepend = \ + set_as_path_top['config'].get( + 'openconfig-routing-policy-ext:asn-list') + if as_path_prepend: + parsed_route_map_stmt_set['as_path_prepend'] = \ + as_path_prepend + + # Fetch community list "delete" config + set_comm_list_delete_top = set_bgp_policy.get('set-community-delete') + if set_comm_list_delete_top: + set_comm_list_delete_config = set_comm_list_delete_top.get('config') + if set_comm_list_delete_config: + comm_list_delete = \ + set_comm_list_delete_config.get('community-set-delete') + if comm_list_delete: + parsed_route_map_stmt_set['comm_list_delete'] = \ + comm_list_delete + + # Fetch community attributes. + self.get_rmap_set_community(set_bgp_policy, parsed_route_map_stmt_set) + + # Fetch extended community attributes. + self.get_rmap_set_extcommunity(set_bgp_policy, parsed_route_map_stmt_set) + + # Fetch other BGP policy "set" attributes + set_bgp_policy_cfg = set_bgp_policy.get('config') + if set_bgp_policy_cfg: + ip_next_hop = set_bgp_policy_cfg.get('set-next-hop') + if ip_next_hop: + parsed_route_map_stmt_set['ip_next_hop'] = ip_next_hop + + ipv6_next_hop_global_addr = set_bgp_policy_cfg.get('set-ipv6-next-hop-global') + ipv6_prefer_global = set_bgp_policy_cfg.get('set-ipv6-next-hop-prefer-global') + if ipv6_next_hop_global_addr or (ipv6_prefer_global is not None): + parsed_route_map_stmt_set['ipv6_next_hop'] = {} + set_ipv6_nexthop = parsed_route_map_stmt_set['ipv6_next_hop'] + if ipv6_next_hop_global_addr: + set_ipv6_nexthop['global_addr'] = ipv6_next_hop_global_addr + if ipv6_prefer_global is not None: + set_ipv6_nexthop['prefer_global'] = ipv6_prefer_global + + local_preference = set_bgp_policy_cfg.get('set-local-pref') + if local_preference: + parsed_route_map_stmt_set['local_preference'] = local_preference + + set_origin = set_bgp_policy_cfg.get('set-route-origin') + if set_origin: + if set_origin == 'EGP': + parsed_route_map_stmt_set['origin'] = 'egp' + elif set_origin == 'IGP': + parsed_route_map_stmt_set['origin'] = 'igp' + elif set_origin == 'INCOMPLETE': + parsed_route_map_stmt_set['origin'] = 'incomplete' + + weight = set_bgp_policy_cfg.get('set-weight') + if weight: + parsed_route_map_stmt_set['weight'] = weight + + @staticmethod + def get_rmap_set_community(set_bgp_policy, parsed_route_map_stmt_set): + '''Parse the "community" sub-section of the BGP policy "set" attribute + portion of the raw input configuration JSON representation. + The BGP policy "set" configuration section to be parsed is specified + by the "set_bgp_policy" input parameter. Parse the information + to convert it to a dictionary matching the "argspec" for the "route_maps" + resource module.''' + + set_community_top = set_bgp_policy.get('set-community') + if (set_community_top and set_community_top.get('inline') and + set_community_top['inline'].get('config') and + set_community_top['inline']['config'].get('communities')): + + set_community_config_list = \ + set_community_top['inline']['config']['communities'] + parsed_route_map_stmt_set['community'] = {} + parsed_rmap_stmt_set_comm = parsed_route_map_stmt_set['community'] + for set_community_config_item in set_community_config_list: + if (set_community_config_item.split(':')[0] in + ('openconfig-bgp-types', 'openconfig-routing-policy-ext')): + set_community_attr = set_community_config_item.split(':')[1] + if not parsed_rmap_stmt_set_comm.get('community_attributes'): + parsed_rmap_stmt_set_comm['community_attributes'] = [] + parsed_comm_attr_list = \ + parsed_rmap_stmt_set_comm['community_attributes'] + comm_attr_rest_to_argspec = { + 'NO_EXPORT_SUBCONFED': 'local_as', + 'NO_ADVERTISE': 'no_advertise', + 'NO_EXPORT': 'no_export', + 'NOPEER': 'no_peer', + 'NONE': 'none', + 'ADDITIVE': 'additive' + } + if set_community_attr in comm_attr_rest_to_argspec: + parsed_comm_attr_list.append( + comm_attr_rest_to_argspec[set_community_attr]) + else: + if not parsed_rmap_stmt_set_comm.get('community_number'): + parsed_rmap_stmt_set_comm['community_number'] = [] + parsed_comm_num_list = \ + parsed_rmap_stmt_set_comm['community_number'] + set_community_num_val_match = \ + re.match(r'\d+:\d+$', set_community_config_item) + if set_community_num_val_match: + parsed_comm_num_list.append(set_community_config_item) + + @staticmethod + def get_rmap_set_extcommunity(set_bgp_policy, parsed_route_map_stmt_set): + '''Parse the "extcommunity" sub-section of the BGP policy "set" + attribute portion of the raw input configuration JSON representation. + The BGP policy "set" configuration section to be parsed is specified + by the "set_bgp_policy" input parameter. Parse the information + to convert it to a dictionary matching the "argspec" for the "route_maps" + resource module.''' + set_extcommunity_top = set_bgp_policy.get('set-ext-community') + if (set_extcommunity_top and set_extcommunity_top.get('inline') and + set_extcommunity_top['inline'].get('config') and + set_extcommunity_top['inline']['config'].get('communities')): + set_extcommunity_config_list = \ + set_extcommunity_top['inline']['config']['communities'] + if set_extcommunity_config_list: + parsed_route_map_stmt_set['extcommunity'] = {} + parsed_rmap_stmt_set_extcomm = parsed_route_map_stmt_set['extcommunity'] + for set_extcommunity_config_item in set_extcommunity_config_list: + if 'route-target:' in set_extcommunity_config_item: + rt_val = set_extcommunity_config_item.replace('route-target:', '') + if parsed_rmap_stmt_set_extcomm.get('rt'): + parsed_rmap_stmt_set_extcomm['rt'].append(rt_val) + else: + parsed_rmap_stmt_set_extcomm['rt'] = [rt_val] + elif 'route-origin:' in set_extcommunity_config_item: + soo_val = set_extcommunity_config_item.replace('route-origin:', '') + if parsed_rmap_stmt_set_extcomm.get('soo'): + parsed_rmap_stmt_set_extcomm['soo'].append(soo_val) + else: + parsed_rmap_stmt_set_extcomm['soo'] = [soo_val] + + @staticmethod + def get_route_map_call_attr(route_map_stmt, parsed_route_map_stmt): + '''Parse the "call" attribute portion of the raw input configuration JSON + representation for the route map "statement" specified + by the "route_map_stmt," input parameter. Parse the information to + convert it to a dictionary matching the "argspec" for the "route_maps" resource + module.''' + + stmt_conditions = route_map_stmt.get('conditions') + if not stmt_conditions: + return + + # Fetch the "call" policy configuration for the route map statement + conditions_config = stmt_conditions.get('config') + if not conditions_config: + return + call_str = conditions_config.get('call-policy') + if not call_str: + return + parsed_route_map_stmt['call'] = call_str + + def get_route_map_stmt_match_attr(self, route_map_stmt, parsed_route_map_stmt): + '''Parse the "match" attributes in the raw input configuration JSON + representation for the route map "statement" specified + by the "route_map_stmt," input parameter. Parse the information to + convert it to a dictionary matching the "argspec" for the "route_maps" resource + module.''' + + # Create a dict object to hold "match" attributes. + parsed_route_map_stmt['match'] = {} + parsed_rmap_match = parsed_route_map_stmt['match'] + + stmt_conditions = route_map_stmt.get('conditions') + if not stmt_conditions: + return + + # Fetch match as-path configuration + if (stmt_conditions.get('match-as-path-set') and + stmt_conditions['match-as-path-set'].get('config')): + as_path = \ + stmt_conditions['match-as-path-set']['config'].get('as-path-set') + if as_path: + parsed_rmap_match['as_path'] = as_path + + # Fetch BGP policy match attributes. + rmap_bgp_policy_match = stmt_conditions.get('openconfig-bgp-policy:bgp-conditions') + if rmap_bgp_policy_match: + self.get_rmap_match_bgp_policy_attr(rmap_bgp_policy_match, parsed_rmap_match) + + # Fetch other match attributes + if (stmt_conditions.get('match-interface') and + stmt_conditions['match-interface'].get('config')): + match_interface = stmt_conditions['match-interface']['config'].get('interface') + if match_interface: + parsed_rmap_match['interface'] = match_interface + + if (stmt_conditions.get('match-prefix-set') and + stmt_conditions['match-prefix-set']['config']): + match_prefix_set = \ + stmt_conditions['match-prefix-set']['config'] + if match_prefix_set and match_prefix_set.get('prefix-set'): + if not parsed_rmap_match.get('ip'): + parsed_rmap_match['ip'] = {} + parsed_rmap_match['ip']['address'] = \ + match_prefix_set['prefix-set'] + if (match_prefix_set and + match_prefix_set.get('openconfig-routing-policy-ext:ipv6-prefix-set')): + parsed_rmap_match['ipv6'] = {} + parsed_rmap_match['ipv6']['address'] = \ + match_prefix_set['openconfig-routing-policy-ext:ipv6-prefix-set'] + + if (stmt_conditions.get('match-neighbor-set') and + stmt_conditions['match-neighbor-set'].get('config') and + stmt_conditions['match-neighbor-set']['config'].get( + 'openconfig-routing-policy-ext:address')): + parsed_rmap_match_peer = stmt_conditions[ + 'match-neighbor-set']['config']['openconfig-routing-policy-ext:address'][0] + parsed_rmap_match['peer'] = {} + if ':' in parsed_rmap_match_peer: + parsed_rmap_match['peer']['ipv6'] = parsed_rmap_match_peer + elif '.' in parsed_rmap_match_peer: + parsed_rmap_match['peer']['ip'] = parsed_rmap_match_peer + else: + parsed_rmap_match['peer']['interface'] = parsed_rmap_match_peer + + if (stmt_conditions.get('config') and + stmt_conditions['config'].get('install-protocol-eq')): + parsed_rmap_match_source_protocol = \ + stmt_conditions['config']['install-protocol-eq'] + if parsed_rmap_match_source_protocol == "openconfig-policy-types:BGP": + parsed_rmap_match['source_protocol'] = "bgp" + elif parsed_rmap_match_source_protocol == "openconfig-policy-types:OSPF": + parsed_rmap_match['source_protocol'] = "ospf" + elif parsed_rmap_match_source_protocol == "openconfig-policy-types:STATIC": + parsed_rmap_match['source_protocol'] = "static" + elif parsed_rmap_match_source_protocol == \ + "openconfig-policy-types:DIRECTLY_CONNECTED": + parsed_rmap_match['source_protocol'] = "connected" + + if stmt_conditions.get( + 'openconfig-routing-policy-ext:match-src-network-instance'): + match_src_vrf = \ + stmt_conditions[ + 'openconfig-routing-policy-ext:match-src-network-instance'].get('config') + if match_src_vrf and match_src_vrf.get('name'): + parsed_rmap_match['source_vrf'] = match_src_vrf['name'] + + if (stmt_conditions.get('match-tag-set') and + stmt_conditions['match-tag-set'].get('config')): + match_tag = \ + stmt_conditions['match-tag-set']['config'].get( + 'openconfig-routing-policy-ext:tag-value') + if match_tag: + parsed_rmap_match['tag'] = match_tag[0] + + @staticmethod + def get_rmap_match_bgp_policy_attr(rmap_bgp_policy_match, parsed_rmap_match): + '''Parse the BGP policy "match" attribute portion of the raw input + configuration JSON representation within the route map "statement" + that is currently being parsed. The configuration section to be parsed + is specified by the "rmap_bgp_match_cfg" input parameter. Parse the + information to convert it to a dictionary matching the "argspec" for + the "route_maps" resource module.''' + + if (rmap_bgp_policy_match.get('match-as-path-set') and + rmap_bgp_policy_match['match-as-path-set'].get('config')): + as_path = rmap_bgp_policy_match['match-as-path-set']['config'].get('as-path-set') + if as_path: + parsed_rmap_match['as_path'] = as_path + + # Fetch BGP policy match "config" attributes + rmap_bgp_match_cfg = rmap_bgp_policy_match.get('config') + if rmap_bgp_match_cfg: + match_metric = rmap_bgp_match_cfg.get('med-eq') + if match_metric: + parsed_rmap_match['metric'] = match_metric + + match_origin = rmap_bgp_match_cfg.get('origin-eq') + if match_origin: + if match_origin == 'IGP': + parsed_rmap_match['origin'] = 'igp' + elif match_origin == 'EGP': + parsed_rmap_match['origin'] = 'egp' + elif match_origin == 'INCOMPLETE': + parsed_rmap_match['origin'] = 'incomplete' + + if rmap_bgp_match_cfg.get('local-pref-eq'): + parsed_rmap_match['local_preference'] = rmap_bgp_match_cfg['local-pref-eq'] + + if rmap_bgp_match_cfg.get('community-set'): + parsed_rmap_match['community'] = rmap_bgp_match_cfg['community-set'] + + if rmap_bgp_match_cfg.get('ext-community-set'): + parsed_rmap_match['ext_comm'] = rmap_bgp_match_cfg['ext-community-set'] + + if rmap_bgp_match_cfg.get('openconfig-bgp-policy-ext:next-hop-set'): + parsed_rmap_match['ip'] = {} + parsed_rmap_match['ip']['next_hop'] = \ + rmap_bgp_match_cfg['openconfig-bgp-policy-ext:next-hop-set'] + + # Fetch BGP policy match "evpn" attributes + if rmap_bgp_policy_match.get('openconfig-bgp-policy-ext:match-evpn-set'): + bgp_policy_match_evpn_cfg = \ + rmap_bgp_policy_match['openconfig-bgp-policy-ext:match-evpn-set'].get('config') + if bgp_policy_match_evpn_cfg: + parsed_rmap_match['evpn'] = {} + if bgp_policy_match_evpn_cfg.get('vni-number'): + parsed_rmap_match['evpn']['vni'] = \ + bgp_policy_match_evpn_cfg.get('vni-number') + if bgp_policy_match_evpn_cfg.get('default-type5-route'): + parsed_rmap_match['evpn']['default_route'] = True + evpn_route_type = bgp_policy_match_evpn_cfg.get('route-type') + if evpn_route_type: + if evpn_route_type == "openconfig-bgp-policy-ext:MACIP": + parsed_rmap_match['evpn']['route_type'] = "macip" + elif evpn_route_type == "openconfig-bgp-policy-ext:MULTICAST": + parsed_rmap_match['evpn']['route_type'] = "multicast" + elif evpn_route_type == "openconfig-bgp-policy-ext:PREFIX": + parsed_rmap_match['evpn']['route_type'] = "prefix" 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 index f83566440..e0d404be7 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( @@ -139,7 +138,7 @@ class Static_routesFacts(object): blackhole = config.get('blackhole', None) track = config.get('track', None) tag = config.get('tag', None) - if blackhole: + if blackhole is not None: index_dict['blackhole'] = blackhole if interface: index_dict['interface'] = interface diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/stp/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/stp/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/stp/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/stp/stp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/stp/stp.py new file mode 100644 index 000000000..da779c502 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/stp/stp.py @@ -0,0 +1,364 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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 stp 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 ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.stp.stp import StpArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + + +stp_map = { + 'openconfig-spanning-tree-types:EDGE_ENABLE': True, + 'openconfig-spanning-tree-types:EDGE_DISABLE': False, + 'openconfig-spanning-tree-types:MSTP': 'mst', + 'openconfig-spanning-tree-ext:PVST': 'pvst', + 'openconfig-spanning-tree-types:RAPID_PVST': 'rapid_pvst', + 'P2P': 'point-to-point', + 'SHARED': 'shared', + 'LOOP': 'loop', + 'ROOT': 'root', + 'NONE': 'none' +} + + +class StpFacts(object): + """ The sonic stp fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = StpArgs.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 stp + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + + if not data: + stp_cfg = self.get_stp_config(self._module) + data = self.update_stp(stp_cfg) + objs = self.render_config(self.generated_spec, data) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': remove_empties(objs)}) + facts['stp'] = 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 update_stp(self, data): + config_dict = {} + if data: + config_dict['global'] = self.update_global(data) + config_dict['interfaces'] = self.update_interfaces(data) + config_dict['mstp'] = self.update_mstp(data) + config_dict['pvst'] = self.update_pvst(data) + config_dict['rapid_pvst'] = self.update_rapid_pvst(data) + + return config_dict + + def update_global(self, data): + global_dict = {} + stp_global = data.get('global', None) + + if stp_global: + config = stp_global.get('config', None) + if config: + enabled_protocol = config.get('enabled-protocol', None) + loop_guard = config.get('loop-guard', None) + bpdu_filter = config.get('bpdu-filter', None) + disabled_vlans = config.get('openconfig-spanning-tree-ext:disabled-vlans', None) + root_guard_timeout = config.get('openconfig-spanning-tree-ext:rootguard-timeout', None) + portfast = config.get('openconfig-spanning-tree-ext:portfast', None) + hello_time = config.get('openconfig-spanning-tree-ext:hello-time', None) + max_age = config.get('openconfig-spanning-tree-ext:max-age', None) + fwd_delay = config.get('openconfig-spanning-tree-ext:forwarding-delay', None) + bridge_priority = config.get('openconfig-spanning-tree-ext:bridge-priority', None) + + if enabled_protocol: + global_dict['enabled_protocol'] = stp_map[enabled_protocol[0]] + if loop_guard is not None: + global_dict['loop_guard'] = loop_guard + if bpdu_filter is not None: + global_dict['bpdu_filter'] = bpdu_filter + if disabled_vlans: + global_dict['disabled_vlans'] = self.convert_vlans_list(disabled_vlans) + if root_guard_timeout: + global_dict['root_guard_timeout'] = root_guard_timeout + if portfast is not None: + global_dict['portfast'] = portfast + if hello_time: + global_dict['hello_time'] = hello_time + if max_age: + global_dict['max_age'] = max_age + if fwd_delay: + global_dict['fwd_delay'] = fwd_delay + if bridge_priority: + global_dict['bridge_priority'] = bridge_priority + + return global_dict + + def update_interfaces(self, data): + interfaces_list = [] + interfaces = data.get('interfaces', None) + + if interfaces: + intf_list = interfaces.get('interface', None) + if intf_list: + for intf in intf_list: + intf_dict = {} + config = intf.get('config', None) + intf_name = config.get('name', None) + edge_port = config.get('edge-port', None) + link_type = config.get('link-type', None) + guard = config.get('guard', None) + bpdu_guard = config.get('bpdu-guard', None) + bpdu_filter = config.get('bpdu-filter', None) + portfast = config.get('openconfig-spanning-tree-ext:portfast', None) + uplink_fast = config.get('openconfig-spanning-tree-ext:uplink-fast', None) + shutdown = config.get('openconfig-spanning-tree-ext:bpdu-guard-port-shutdown', None) + cost = config.get('openconfig-spanning-tree-ext:cost', None) + port_priority = config.get('openconfig-spanning-tree-ext:port-priority', None) + stp_enable = config.get('openconfig-spanning-tree-ext:spanning-tree-enable', None) + + if intf_name: + intf_dict['intf_name'] = intf_name + if edge_port is not None: + intf_dict['edge_port'] = stp_map[edge_port] + if link_type: + intf_dict['link_type'] = stp_map[link_type] + if guard: + intf_dict['guard'] = stp_map[guard] + if bpdu_guard is not None: + intf_dict['bpdu_guard'] = bpdu_guard + if bpdu_filter is not None: + intf_dict['bpdu_filter'] = bpdu_filter + if portfast is not None: + intf_dict['portfast'] = portfast + if uplink_fast is not None: + intf_dict['uplink_fast'] = uplink_fast + if shutdown is not None: + intf_dict['shutdown'] = shutdown + if cost: + intf_dict['cost'] = cost + if port_priority: + intf_dict['port_priority'] = port_priority + if stp_enable is not None: + intf_dict['stp_enable'] = stp_enable + if intf_dict: + interfaces_list.append(intf_dict) + + return interfaces_list + + def update_mstp(self, data): + mstp_dict = {} + mstp = data.get('mstp', None) + + if mstp: + config = mstp.get('config', None) + mst_instances = mstp.get('mst-instances', None) + interfaces = mstp.get('interfaces', None) + if config: + mst_name = config.get('name', None) + revision = config.get('revision', None) + max_hop = config.get('max-hop', None) + hello_time = config.get('hello-time', None) + max_age = config.get('max-age', None) + fwd_delay = config.get('forwarding-delay', None) + + if mst_name: + mstp_dict['mst_name'] = mst_name + if revision: + mstp_dict['revision'] = revision + if max_hop: + mstp_dict['max_hop'] = max_hop + if hello_time: + mstp_dict['hello_time'] = hello_time + if max_age: + mstp_dict['max_age'] = max_age + if fwd_delay: + mstp_dict['fwd_delay'] = fwd_delay + + if mst_instances: + mst_instance = mst_instances.get('mst-instance', None) + if mst_instance: + mst_instances_list = [] + for inst in mst_instance: + inst_dict = {} + mst_id = inst.get('mst-id', None) + config = inst.get('config', None) + interfaces = inst.get('interfaces', None) + if mst_id: + inst_dict['mst_id'] = mst_id + if interfaces: + intf_list = self.get_interfaces_list(interfaces) + if intf_list: + inst_dict['interfaces'] = intf_list + if config: + vlans = config.get('vlan', None) + bridge_priority = config.get('bridge-priority', None) + if vlans: + inst_dict['vlans'] = self.convert_vlans_list(vlans) + if bridge_priority: + inst_dict['bridge_priority'] = bridge_priority + if inst_dict: + mst_instances_list.append(inst_dict) + if mst_instances_list: + mstp_dict['mst_instances'] = mst_instances_list + + return mstp_dict + + def update_pvst(self, data): + pvst_list = [] + pvst = data.get('openconfig-spanning-tree-ext:pvst', None) + + if pvst: + vlans = pvst.get('vlans', None) + if vlans: + vlans_list = self.get_vlans_list(vlans) + if vlans_list: + pvst_list = vlans_list + + return pvst_list + + def update_rapid_pvst(self, data): + rapid_pvst_list = [] + rapid_pvst = data.get('rapid-pvst', None) + + if rapid_pvst: + vlans = rapid_pvst.get('vlan', None) + if vlans: + vlans_list = self.get_vlans_list(vlans) + if vlans_list: + rapid_pvst_list = vlans_list + + return rapid_pvst_list + + def get_stp_config(self, module): + stp_cfg = None + get_stp_path = '/data/openconfig-spanning-tree:stp' + request = {'path': get_stp_path, 'method': 'get'} + + try: + response = edit_config(module, to_request(module, request)) + stp_cfg = response[0][1].get('openconfig-spanning-tree:stp', None) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + return stp_cfg + + def get_interfaces_list(self, data): + intf_list = [] + interface_list = data.get('interface', None) + + if interface_list: + for intf in interface_list: + intf_dict = {} + config = intf.get('config', None) + if config: + intf_name = config.get('name', None) + cost = config.get('cost', None) + port_priority = config.get('port-priority', None) + + if intf_name: + intf_dict['intf_name'] = intf_name + if cost: + intf_dict['cost'] = cost + if port_priority: + intf_dict['port_priority'] = port_priority + if intf_dict: + intf_list.append(intf_dict) + + return intf_list + + def get_vlans_list(self, data): + vlan_list = [] + + for vlan in data: + vlan_dict = {} + vlan_id = vlan.get('vlan-id') + config = vlan.get('config', None) + interfaces = vlan.get('interfaces', None) + + if vlan_id: + vlan_dict['vlan_id'] = vlan_id + if interfaces: + intf_list = self.get_interfaces_list(interfaces) + if intf_list: + vlan_dict['interfaces'] = intf_list + if config: + hello_time = config.get('hello-time', None) + max_age = config.get('max-age', None) + fwd_delay = config.get('forwarding-delay', None) + bridge_priority = config.get('bridge-priority', None) + + if hello_time: + vlan_dict['hello_time'] = hello_time + if max_age: + vlan_dict['max_age'] = max_age + if fwd_delay: + vlan_dict['fwd_delay'] = fwd_delay + if bridge_priority: + vlan_dict['bridge_priority'] = bridge_priority + if vlan_dict: + vlan_list.append(vlan_dict) + + return vlan_list + + def convert_vlans_list(self, vlans): + converted_vlans = [] + + for vlan in vlans: + if isinstance(vlan, int): + converted_vlans.append(str(vlan)) + + else: + converted_vlans.append(vlan.replace('..', '-')) + + return converted_vlans 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 index 1d7a82d83..65c4491d3 100644 --- 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 @@ -11,7 +11,6 @@ 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 ( 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 index a1e79910f..b752b7a83 100644 --- 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 @@ -11,8 +11,6 @@ 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 ( 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 index 038e97f83..59f08e63e 100644 --- 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 @@ -11,7 +11,6 @@ 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 ( @@ -22,6 +21,9 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.s to_request, edit_config ) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + remove_empties_from_list +) from ansible.module_utils.connection import ConnectionError GET = "get" @@ -74,8 +76,9 @@ class UsersFacts(object): if objs: facts['users'] = [] params = utils.validate_config(self.argument_spec, {'config': objs}) + if params: - facts['users'].extend(params['config']) + facts['users'].extend(remove_empties_from_list(params['config'])) ansible_facts['ansible_network_resources'].update(facts) return ansible_facts @@ -94,7 +97,7 @@ class UsersFacts(object): 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}] + request = [{"path": "data/openconfig-system:system/aaa/authentication/users", "method": GET}] users = [] try: response = edit_config(self._module, to_request(self._module, request)) @@ -102,21 +105,16 @@ class UsersFacts(object): 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', []) + if "openconfig-system:users" in response[0][1]: + raw_users = response[0][1].get("openconfig-system:users", {}).get('user', []) 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) + role = raw_user.get('config', {}).get('role', 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/vlan_mapping/__init__.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlan_mapping/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlan_mapping/__init__.py diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlan_mapping/vlan_mapping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlan_mapping/vlan_mapping.py new file mode 100644 index 000000000..ac53415c7 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/facts/vlan_mapping/vlan_mapping.py @@ -0,0 +1,225 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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 vlan_mapping 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 ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.vlan_mapping.vlan_mapping import Vlan_mappingArgs + +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.module_utils.connection import ConnectionError + + +class Vlan_mappingFacts(object): + """ The sonic vlan_mapping fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Vlan_mappingArgs.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 vlan_mapping + :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 + + all_vlan_mapping_configs = {} + if not data: + vlan_mapping_configs = self.get_vlan_mappings() + for interface, vlan_config in vlan_mapping_configs.items(): + vlan_mapping_configs_dict = {} + vlan_mapping_configs_dict['name'] = interface + vlan_mapping_configs_dict['mapping'] = vlan_config + all_vlan_mapping_configs[interface] = vlan_mapping_configs_dict + + objs = [] + for vlan_mapping_config in all_vlan_mapping_configs.items(): + obj = self.render_config(self.generated_spec, vlan_mapping_config) + if obj: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('vlan_mapping', None) + facts = {} + if objs: + params = utils.validate_config(self.argument_spec, {'config': objs}) + facts['vlan_mapping'] = 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) + config['name'] = conf[1]['name'] + config['mapping'] = conf[1]['mapping'] + + return utils.remove_empties(config) + + def get_vlan_mappings(self): + """Get all vlan mappings on device""" + interfaces = self.get_ports() + self.get_portchannels() + + vlan_mapping_configs = {} + for interface in interfaces: + response = self.get_port_mappings(interface) + if "openconfig-interfaces-ext:mapped-vlans" in response: + vlan_list = response["openconfig-interfaces-ext:mapped-vlans"].get("mapped-vlan", {}) + for vlan_mapping in vlan_list: + vlan_mapping_dict = {} + vlan_mapping_dict["vlan_ids"] = [] + + tmp_dot1q_tunnel = (vlan_mapping + .get("egress-mapping", {}) + .get("config", {}) + .get("vlan-stack-action", "SWAP")) + if tmp_dot1q_tunnel == "SWAP": + vlan_mapping_dict["dot1q_tunnel"] = False + vlan_mapping_dict["inner_vlan"] = (vlan_mapping + .get("match", {}) + .get("double-tagged", {}) + .get("config", {}) + .get("inner-vlan-id", None)) + if vlan_mapping_dict["inner_vlan"]: + (vlan_mapping_dict["vlan_ids"] + .append(vlan_mapping.get("match", {}) + .get("double-tagged", {}) + .get("config", {}) + .get("outer-vlan-id", None))) + else: + (vlan_mapping_dict["vlan_ids"] + .append(vlan_mapping.get("match", {}) + .get("single-tagged", {}) + .get("config", {}) + .get("vlan-ids", None))) + if vlan_mapping_dict["vlan_ids"]: + vlan_mapping_dict["vlan_ids"][0] = vlan_mapping_dict["vlan_ids"][0][0] + else: + vlan_mapping_dict["dot1q_tunnel"] = True + tmp_vlan_ids = (vlan_mapping + .get("match", {}) + .get("single-tagged", {}) + .get("config", {}) + .get("vlan-ids", None)) + if tmp_vlan_ids: + vlan_mapping_dict["vlan_ids"].extend(tmp_vlan_ids[0].replace('..', '-').split(',')) + + vlan_mapping_dict["service_vlan"] = vlan_mapping.get("vlan-id", None) + vlan_mapping_dict["priority"] = (vlan_mapping + .get("egress-mapping", {}) + .get("config", {}) + .get("mapped-vlan-priority", None)) + + if interface["ifname"] in vlan_mapping_configs: + vlan_mapping_configs[interface["ifname"]].append(vlan_mapping_dict) + else: + vlan_mapping_configs[interface["ifname"]] = [] + vlan_mapping_configs[interface["ifname"]].append(vlan_mapping_dict) + + return vlan_mapping_configs + + def get_port_mappings(self, interface): + """Get a ports vlan mappings from device""" + ifname = interface["ifname"] + if '/' in ifname: + ifname = ifname.replace('/', '%2F') + + port_mappings = "data/openconfig-interfaces:interfaces/interface=%s/openconfig-interfaces-ext:mapped-vlans" % ifname + method = "GET" + request = [{"path": port_mappings, "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) + + return response[0][1] + + def get_ports(self): + """Get all port names on device""" + all_ports_path = "data/sonic-port:sonic-port/PORT_TABLE" + method = "GET" + request = [{"path": all_ports_path, "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) + + response = response[0][1] + + port_list = [] + + if "sonic-port:PORT_TABLE" in response: + component = response["sonic-port:PORT_TABLE"] + if "PORT_TABLE_LIST" in component: + for port in component["PORT_TABLE_LIST"]: + if "Eth" in port["ifname"]: + port_list.append({"ifname": port["ifname"]}) + + return port_list + + def get_portchannels(self): + """Get all portchannel names on device""" + all_portchannels_path = "data/sonic-portchannel:sonic-portchannel" + method = "GET" + request = [{"path": all_portchannels_path, "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) + + response = response[0][1] + + portchannel_list = [] + + if "sonic-portchannel:sonic-portchannel" in response: + component = response["sonic-portchannel:sonic-portchannel"] + if "PORTCHANNEL" in component: + component = component["PORTCHANNEL"] + if "PORTCHANNEL_LIST" in component: + component = component["PORTCHANNEL_LIST"] + for portchannel in component: + portchannel_list.append({"ifname": portchannel["name"]}) + + return portchannel_list 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 index 7c4af2ea8..3df200048 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( 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 index 797612bc4..375c453d5 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( 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 index 51aec6561..e521313b8 100644 --- 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 @@ -13,7 +13,6 @@ 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 ( @@ -166,10 +165,9 @@ class VxlansFacts(object): 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 + 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): 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 index 77a63d425..30739ef82 100644 --- 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 @@ -33,7 +33,6 @@ 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 @@ -132,7 +131,7 @@ def edit_config(module, commands, skip_code=None): # 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: + if isinstance(request, dict): url = request.get("path", None) if url: request["path"] = update_url(url) 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 index 7471bcb11..9c2d18a52 100644 --- 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 @@ -13,16 +13,10 @@ 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 @@ -195,11 +189,6 @@ def get_peergroups(module, vrf_name): 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']: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/formatted_diff_utils.py b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/formatted_diff_utils.py new file mode 100644 index 000000000..f6385294f --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/module_utils/network/sonic/utils/formatted_diff_utils.py @@ -0,0 +1,588 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2023 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) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +from copy import ( + deepcopy +) +from difflib import ( + context_diff +) + + +def get_key_sets(dict_conf): + key_set = set(dict_conf.keys()) + trival_key_set = set() + dict_list_key_set = set() + for key in key_set: + if dict_conf[key] not in [None, [], {}]: + if isinstance(dict_conf[key], (list, dict)): + dict_list_key_set.add(key) + else: + trival_key_set.add(key) + return trival_key_set, dict_list_key_set + + +def get_test_key_set(key, test_keys): + tst_keys = deepcopy(test_keys) + t_key_set = set() + if not tst_keys or not key: + return t_key_set + + t_keys = next((t_key_item[key] for t_key_item in tst_keys if key in t_key_item), None) + if t_keys: + t_keys.pop('__merge_op', None) + t_keys.pop('__delete_op', None) + t_keys.pop('__key_match_op', None) + t_key_set = set(t_keys.keys()) + + return t_key_set + + +# +# Pre-defined Key Match Operations +# + + +""" +Default key match operation. +""" + + +def __KEY_MATCH_OP_DEFAULT(key_set, command, exist_conf): + trival_cmd_key_set, dict_list_cmd_key_set = get_key_sets(command) + trival_exist_key_set, dict_list_exist_key_set = get_key_sets(exist_conf) + + common_trival_key_set = trival_cmd_key_set.intersection(trival_exist_key_set) + common_dict_list_key_set = dict_list_cmd_key_set.intersection(dict_list_exist_key_set) + + key_matched_cnt = 0 + for key in common_trival_key_set.union(common_dict_list_key_set): + if command[key] == exist_conf[key]: + if key in key_set: + key_matched_cnt += 1 + + key_matched = (key_matched_cnt == len(key_set)) + return key_matched + + +def get_key_match_op(key, test_keys): + k_match_op = __KEY_MATCH_OP_DEFAULT + t_key_set = set() + if not test_keys or not key: + return k_match_op + + t_keys = next((t_key_item[key] for t_key_item in test_keys if key in t_key_item), None) + if t_keys: + k_match_op = t_keys.get('__key_match_op', __KEY_MATCH_OP_DEFAULT) + + return k_match_op + + +# +# Pre-defined Merge Operations +# + + +""" +Default key match operation: simply merge command to existing config. +""" + + +def __MERGE_OP_DEFAULT(key_set, command, exist_conf): + new_conf = exist_conf + trival_cmd_key_set, dict_list_cmd_key_set = get_key_sets(command) + nu, dict_list_exist_key_set = get_key_sets(new_conf) + + for key in trival_cmd_key_set: + new_conf[key] = command[key] + + only_cmd_dict_list_key_set = dict_list_cmd_key_set.difference(dict_list_exist_key_set) + for key in only_cmd_dict_list_key_set: + new_conf[key] = command[key] + + return False, new_conf + + +def get_merge_op(key, test_keys): + mrg_op = __MERGE_OP_DEFAULT + if not test_keys: + return mrg_op + if not key: + key = '__default_ops' + t_keys = next((t_key_item[key] for t_key_item in test_keys if key in t_key_item), None) + if t_keys: + mrg_op = t_keys.get('__merge_op', __MERGE_OP_DEFAULT) + + return mrg_op + + +# +# Pre-defined Delete Operations +# + + +""" +Delete entire configuration. +""" + + +def __DELETE_CONFIG(key_set, command, exist_conf): + new_conf = [] + return True, new_conf + + +""" +Delete entire configuration if there is no sub-configuration. +""" + + +def __DELETE_CONFIG_IF_NO_SUBCONFIG(key_set, command, exist_conf): + nu, dict_list_cmd_key_set = get_key_sets(command) + if len(dict_list_cmd_key_set) == 0: + new_conf = [] + return True, new_conf + else: + new_conf = exist_conf + return False, new_conf + + +""" +Delete sub-configuration and leaf configuration, if any. +""" + + +def __DELETE_SUBCONFIG_AND_LEAFS(key_set, command, exist_conf): + new_conf = exist_conf + + trival_cmd_key_set, dict_list_cmd_key_set = get_key_sets(command) + trival_cmd_key_not_key_set = trival_cmd_key_set.difference(key_set) + for key in trival_cmd_key_not_key_set: + new_conf.pop(key, None) + + nu, dict_list_exist_key_set = get_key_sets(exist_conf) + common_dict_list_key_set = dict_list_cmd_key_set.intersection(dict_list_exist_key_set) + if len(common_dict_list_key_set) != 0: + for key in common_dict_list_key_set: + new_conf.pop(key, None) + + return True, new_conf + + +""" +Delete sub-configuration only, if any. +""" + + +def __DELETE_SUBCONFIG_ONLY(key_set, command, exist_conf): + new_conf = exist_conf + nu, dict_list_cmd_key_set = get_key_sets(command) + nu, dict_list_exist_key_set = get_key_sets(exist_conf) + common_dict_list_key_set = dict_list_cmd_key_set.intersection(dict_list_exist_key_set) + for key in common_dict_list_key_set: + new_conf.pop(key, None) + return True, new_conf + + +""" +Delete configuration if there is no non-key leaf, and +delete non-key leaf configuration, if any. +""" + + +def __DELETE_LEAFS_OR_CONFIG_IF_NO_NON_KEY_LEAF(key_set, command, exist_conf): + new_conf = exist_conf + trival_cmd_key_set, dict_list_cmd_key_set = get_key_sets(command) + + if (len(key_set) == len(trival_cmd_key_set)) and \ + (len(dict_list_cmd_key_set) == 0): + new_conf = [] + return True, new_conf + + trival_cmd_key_not_key_set = trival_cmd_key_set.difference(key_set) + for key in trival_cmd_key_not_key_set: + new_conf.pop(key, None) + + return False, new_conf + + +""" +This is default deletion operation. +Delete configuration if there is no non-key leaf, and +delete non-key leaf configuration, if any, if the values of non-key leaf are +equal between command and existing configuration. +""" + + +def __DELETE_OP_DEFAULT(key_set, command, exist_conf): + new_conf = exist_conf + trival_cmd_key_set, dict_list_cmd_key_set = get_key_sets(command) + + if (len(key_set) == len(trival_cmd_key_set)) and \ + (len(dict_list_cmd_key_set) == 0): + new_conf = [] + return True, new_conf + + trival_cmd_key_not_key_set = trival_cmd_key_set.difference(key_set) + for key in trival_cmd_key_not_key_set: + command_val = command.get(key, None) + new_conf_val = new_conf.get(key, None) + if command_val == new_conf_val: + new_conf.pop(key, None) + + return False, new_conf + + +def get_delete_op(key, test_keys): + del_op = __DELETE_OP_DEFAULT + if not test_keys: + return del_op + if not key: + key = '__default_ops' + t_keys = next((t_key_item[key] for t_key_item in test_keys if key in t_key_item), None) + if t_keys: + del_op = t_keys.get('__delete_op', __DELETE_OP_DEFAULT) + + return del_op + + +def get_new_config(commands, exist_conf, test_keys=None): + + if not commands: + return exist_conf + + cmds = deepcopy(commands) + + n_conf = list() + e_conf = exist_conf + for cmd in cmds: + state = cmd['state'] + cmd.pop('state') + + if state == 'merged': + n_conf = derive_config_from_merged_cmd(cmd, e_conf, test_keys) + elif state == 'deleted': + n_conf = derive_config_from_deleted_cmd(cmd, e_conf, test_keys) + elif state == 'replaced': + n_conf = derive_config_from_merged_cmd(cmd, e_conf, test_keys) + elif state == 'overridden': + n_conf = derive_config_from_merged_cmd(cmd, e_conf, test_keys) + # If the "cmd" is derived from playbook, that is "want", the below + # line should be good enough: + # n_conf = cmd + + e_conf = n_conf + + return n_conf + + +def derive_config_from_merged_cmd(command, exist_conf, test_keys=None): + + if not command: + return exist_conf + + if isinstance(command, list) and isinstance(exist_conf, list): + nu, new_conf_dict = derive_config_from_merged_cmd_dict({"config": command}, + {"config": exist_conf}, + test_keys) + new_conf = new_conf_dict.get("config", []) + elif isinstance(command, dict) and isinstance(exist_conf, dict): + merge_op_dft = get_merge_op('__default_ops', test_keys) + nu, new_conf = derive_config_from_merged_cmd_dict(command, exist_conf, + test_keys, None, + None, merge_op_dft) + elif isinstance(command, dict) and isinstance(exist_conf, list): + nu, new_conf_dict = derive_config_from_merged_cmd_dict({"config": [command]}, + {"config": exist_conf}, + test_keys) + new_conf = new_conf_dict.get("config", []) + else: + new_conf = exist_conf + + return new_conf + + +def derive_config_from_merged_cmd_dict(command, exist_conf, test_keys=None, key_set=None, + key_match_op=None, merge_op=None): + + if test_keys is None: + test_keys = [] + if key_set is None: + key_set = set() + if key_match_op is None: + key_match_op = __KEY_MATCH_OP_DEFAULT + if merge_op is None: + merge_op = __MERGE_OP_DEFAULT + + new_conf = deepcopy(exist_conf) + if not command: + return False, new_conf + + trival_cmd_key_set, dict_list_cmd_key_set = get_key_sets(command) + trival_exist_key_set, dict_list_exist_key_set = get_key_sets(new_conf) + + common_trival_key_set = trival_cmd_key_set.intersection(trival_exist_key_set) + common_dict_list_key_set = dict_list_cmd_key_set.intersection(dict_list_exist_key_set) + + key_matched = key_match_op(key_set, command, new_conf) + if key_matched: + done, new_conf = merge_op(key_set, command, new_conf) + if done: + return key_matched, new_conf + else: + nu, dict_list_exist_key_set = get_key_sets(new_conf) + common_dict_list_key_set = dict_list_cmd_key_set.intersection(dict_list_exist_key_set) + else: + return key_matched, new_conf + + for key in key_set: + common_dict_list_key_set.discard(key) + + for key in common_dict_list_key_set: + + cmd_value = command[key] + exist_value = new_conf[key] + + t_key_set = get_test_key_set(key, test_keys) + t_key_match_op = get_key_match_op(key, test_keys) + t_merge_op = get_merge_op(key, test_keys) + + if (isinstance(cmd_value, list) and isinstance(exist_value, list)): + c_list = cmd_value + e_list = exist_value + + new_conf_list = list() + not_dict_item = False + dict_no_key_item = False + for c_item in c_list: + matched_key_dict = False + for e_item in e_list: + if (isinstance(c_item, dict) and isinstance(e_item, dict)): + if t_key_set: + remaining_keys = [t_key_item for t_key_item in test_keys if key not in t_key_item] + k_mtchd, new_conf_dict = derive_config_from_merged_cmd_dict(c_item, + e_item, + remaining_keys, + t_key_set, + t_key_match_op, + t_merge_op) + if k_mtchd: + new_conf[key].remove(e_item) + if new_conf_dict: + new_conf_list.append(new_conf_dict) + matched_key_dict = True + break + else: + dict_no_key_item = True + break + + else: + not_dict_item = True + break + + if not matched_key_dict: + new_conf_list.append(c_item) + + if not_dict_item or dict_no_key_item: + break + + if dict_no_key_item: + new_conf_list = e_list + c_list + + if not_dict_item: + c_set = set(c_list) + e_set = set(e_list) + merge_set = c_set.union(e_set) + if merge_set: + new_conf[key] = list(merge_set) + elif new_conf_list: + new_conf[key].extend(new_conf_list) + + elif (isinstance(cmd_value, dict) and isinstance(exist_value, dict)): + k_mtchd, new_conf_dict = derive_config_from_merged_cmd_dict(cmd_value, + exist_value, + test_keys, + None, + t_key_match_op, + t_merge_op) + if k_mtchd and new_conf_dict: + new_conf[key] = new_conf_dict + + elif (isinstance(cmd_value, (list, dict)) or isinstance(exist_value, (list, dict))): + new_conf[key] = exist_value + break + + else: + continue + + return key_matched, new_conf + + +def derive_config_from_deleted_cmd(command, exist_conf, test_keys=None): + + if not command or not exist_conf: + return exist_conf + + if isinstance(command, list) and isinstance(exist_conf, list): + nu, new_conf_dict = derive_config_from_deleted_cmd_dict({"config": command}, + {"config": exist_conf}, + test_keys) + new_conf = new_conf_dict.get("config", []) + elif isinstance(command, dict) and isinstance(exist_conf, dict): + delete_op_dft = get_delete_op('__default_ops', test_keys) + nu, new_conf = derive_config_from_deleted_cmd_dict(command, exist_conf, + test_keys, None, + None, delete_op_dft) + elif isinstance(command, dict) and isinstance(exist_conf, list): + nu, new_conf_dict = derive_config_from_deleted_cmd_dict({"config": [command]}, + {"config": exist_conf}, + test_keys) + new_conf = new_conf_dict.get("config", []) + else: + new_conf = exist_conf + + return new_conf + + +def derive_config_from_deleted_cmd_dict(command, exist_conf, test_keys=None, key_set=None, + key_match_op=None, delete_op=None): + + if test_keys is None: + test_keys = [] + if key_set is None: + key_set = set() + if key_match_op is None: + key_match_op = __KEY_MATCH_OP_DEFAULT + if delete_op is None: + delete_op = __DELETE_OP_DEFAULT + + new_conf = deepcopy(exist_conf) + if not command: + return True, [] + + trival_cmd_key_set, dict_list_cmd_key_set = get_key_sets(command) + trival_exist_key_set, dict_list_exist_key_set = get_key_sets(new_conf) + + common_trival_key_set = trival_cmd_key_set.intersection(trival_exist_key_set) + common_dict_list_key_set = dict_list_cmd_key_set.intersection(dict_list_exist_key_set) + + key_matched = key_match_op(key_set, command, new_conf) + if key_matched: + done, new_conf = delete_op(key_set, command, new_conf) + if done: + return key_matched, new_conf + else: + nu, dict_list_exist_key_set = get_key_sets(new_conf) + common_dict_list_key_set = dict_list_cmd_key_set.intersection(dict_list_exist_key_set) + else: + return key_matched, new_conf + + for key in key_set: + common_dict_list_key_set.discard(key) + + for key in common_dict_list_key_set: + + cmd_value = command[key] + exist_value = new_conf[key] + + t_key_set = get_test_key_set(key, test_keys) + t_key_match_op = get_key_match_op(key, test_keys) + t_delete_op = get_delete_op(key, test_keys) + + if (isinstance(cmd_value, list) and isinstance(exist_value, list)): + c_list = cmd_value + e_list = exist_value + + new_conf_list = list() + not_dict_item = False + dict_no_key_item = False + for c_item in c_list: + for e_item in e_list: + if (isinstance(c_item, dict) and isinstance(e_item, dict)): + if t_key_set: + remaining_keys = [t_key_item for t_key_item in test_keys if key not in t_key_item] + k_mtchd, new_conf_dict = derive_config_from_deleted_cmd_dict(c_item, e_item, + remaining_keys, + t_key_set, + t_key_match_op, + t_delete_op) + if k_mtchd: + new_conf[key].remove(e_item) + if new_conf_dict: + new_conf_list.append(new_conf_dict) + break + else: + dict_no_key_item = True + break + + else: + not_dict_item = True + break + + if not_dict_item or dict_no_key_item: + break + + if dict_no_key_item: + new_conf_list = e_list + + if not_dict_item: + c_set = set(c_list) + e_set = set(e_list) + delete_set = e_set.difference(c_set) + if delete_set: + new_conf[key] = list(delete_set) + else: + new_conf[key] = [] + elif new_conf_list: + new_conf[key].extend(new_conf_list) + + elif (isinstance(cmd_value, dict) and isinstance(exist_value, dict)): + k_mtchd, new_conf_dict = derive_config_from_deleted_cmd_dict(cmd_value, + exist_value, + test_keys, + None, + t_key_match_op, + t_delete_op) + if k_mtchd: + new_conf.pop(key, None) + if new_conf_dict: + new_conf[key] = new_conf_dict + + elif (isinstance(cmd_value, (list, dict)) or isinstance(exist_value, (list, dict))): + new_conf[key] = exist_value + break + + else: + continue + + return key_matched, new_conf + + +def get_formatted_config_diff(exist_conf, new_conf, verbosity=0): + + exist_conf = json.dumps(exist_conf, sort_keys=True, indent=4, separators=(u',', u': ')) + u'\n' + new_conf = json.dumps(new_conf, sort_keys=True, indent=4, separators=(u',', u': ')) + u'\n' + + bfr = exist_conf.replace("\"", "\'") + aft = new_conf.replace("\"", "\'") + + bfr_list = bfr.splitlines(True) + aft_list = aft.splitlines(True) + diffs = context_diff(bfr_list, aft_list, fromfile='before', tofile='after') + + if verbosity >= 3: + formatted_diff = list() + for diff in diffs: + formatted_diff.append(diff.rstrip('\n')) + + else: + formatted_diff = {'prepared': u''.join(diffs)} + + return formatted_diff 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 index a7f6e9063..60df9251d 100644 --- 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 @@ -27,10 +27,16 @@ __metaclass__ = type import traceback import json +import re from ansible.module_utils._text import to_native try: + from urllib import quote +except ImportError: + from urllib.parse import quote + +try: import jinja2 HAS_LIB = True except Exception as e: @@ -38,6 +44,29 @@ except Exception as e: ERR_MSG = to_native(e) LIB_IMP_ERR = traceback.format_exc() +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) + +intf_speed_map = { + 0: 'SPEED_DEFAULT', + 10: "SPEED_10MB", + 100: "SPEED_100MB", + 1000: "SPEED_1GB", + 2500: "SPEED_2500MB", + 5000: "SPEED_5GB", + 10000: "SPEED_10GB", + 20000: "SPEED_20GB", + 25000: "SPEED_25GB", + 40000: "SPEED_40GB", + 50000: "SPEED_50GB", + 100000: "SPEED_100GB", + 200000: "SPEED_200GB", + 400000: "SPEED_400GB", + 800000: "SPEED_800GB" +} + # To create Loopback, VLAN interfaces def build_interfaces_create_request(interface_name): @@ -53,3 +82,60 @@ def build_interfaces_create_request(interface_name): "method": method, "data": ret_payload} return request + + +def retrieve_port_group_interfaces(module): + port_group_interfaces = [] + method = "get" + port_num_regex = re.compile(r'[\d]{1,4}$') + port_group_url = 'data/openconfig-port-group:port-groups' + request = {"path": port_group_url, "method": method} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + if 'openconfig-port-group:port-groups' in response[0][1] and "port-group" in response[0][1]['openconfig-port-group:port-groups']: + port_groups = response[0][1]['openconfig-port-group:port-groups']['port-group'] + for pg_config in port_groups: + if 'state' in pg_config: + member_start = pg_config['state'].get('member-if-start', '') + member_start = re.search(port_num_regex, member_start) + member_end = pg_config['state'].get('member-if-end', '') + member_end = re.search(port_num_regex, member_end) + if member_start and member_end: + member_start = int(member_start.group(0)) + member_end = int(member_end.group(0)) + port_group_interfaces.extend(range(member_start, member_end + 1)) + + return port_group_interfaces + + +def retrieve_default_intf_speed(module, intf_name): + + # Read the valid_speeds + dft_intf_speed = 'SPEED_DEFAULT' + method = "get" + sonic_port_url = 'data/sonic-port:sonic-port/PORT/PORT_LIST=%s' + sonic_port_vs_url = (sonic_port_url + '/valid_speeds') % quote(intf_name, safe='') + request = {"path": sonic_port_vs_url, "method": method} + try: + response = edit_config(module, to_request(module, request)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + if 'sonic-port:valid_speeds' in response[0][1]: + v_speeds = response[0][1].get('sonic-port:valid_speeds', '') + v_speeds_list = v_speeds.split(",") + v_speeds_int_list = [] + for vs in v_speeds_list: + v_speeds_int_list.append(int(vs)) + + dft_speed_int = 0 + if v_speeds_int_list: + dft_speed_int = max(v_speeds_int_list) + dft_intf_speed = intf_speed_map.get(dft_speed_int, 'SPEED_DEFAULT') + + if dft_intf_speed == 'SPEED_DEFAULT': + module.fail_json(msg="Unable to retireve default port speed for the interface {0}".format(intf_name)) + + return dft_intf_speed 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 index 0d6e6d1a0..bc865790b 100644 --- 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 @@ -13,12 +13,17 @@ __metaclass__ = type import re import json import ast +from copy import copy +from itertools import (count, groupby) from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_empties +) +from ansible.module_utils.common.network import ( is_masklen, to_netmask, - remove_empties ) +from ansible.module_utils.common.validation import check_required_arguments from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( to_request, edit_config @@ -31,6 +36,21 @@ GET = 'get' intf_naming_mode = "" +def remove_matching_defaults(root, default_entry): + if isinstance(root, list): + for list_item in root: + remove_matching_defaults(list_item, default_entry) + elif isinstance(root, dict): + nextobj = root.get(default_entry[0]['name']) + if nextobj is not None: + if len(default_entry) > 1: + remove_matching_defaults(nextobj, default_entry[1:]) + else: + # Leaf + if nextobj == default_entry[0]['default']: + root.pop(default_entry[0]['name']) + + def get_diff(base_data, compare_with_data, test_keys=None, is_skeleton=None): diff = [] if is_skeleton is None: @@ -319,10 +339,13 @@ def netmask_to_cidr(netmask): def remove_empties_from_list(config_list): ret_config = [] - if not config_list: + if not config_list or not isinstance(config_list, list): return ret_config for config in config_list: - ret_config.append(remove_empties(config)) + if isinstance(config, dict): + ret_config.append(remove_empties(config)) + else: + ret_config.append(copy(config)) return ret_config @@ -432,14 +455,7 @@ def get_normalize_interface_name(intf_name, module): 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 + return 'SPEED_' + breakout_mode.split('x')[1].replace('G', 'GB') def get_breakout_mode(module, name): @@ -455,7 +471,7 @@ def get_breakout_mode(module, name): 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']: + if json_obj and isinstance(json_obj, dict) and 404 == json_obj['code']: response = None else: module.fail_json(msg=str(exc), code=exc.code) @@ -509,3 +525,205 @@ def command_list_str_to_dict(module, warnings, cmd_list_in, exec_cmd=False): cmd_list_out.append(cmd_out) return cmd_list_out + + +def send_requests(module, requests): + + reply = dict() + response = [] + if not module.check_mode and requests: + try: + response = edit_config(module, to_request(module, requests)) + except ConnectionError as exc: + module.fail_json(msg=str(exc), code=exc.code) + + reply = response[0][1] + + return reply + + +def get_replaced_config(new_conf, exist_conf, test_keys=None): + + replace_conf = [] + if not new_conf or not exist_conf: + return replace_conf + + if isinstance(new_conf, list) and isinstance(exist_conf, list): + + replace_conf_dict = get_replaced_config_dict({"config": new_conf}, + {"config": exist_conf}, + test_keys) + replaced_conf = replace_conf_dict.get("config", []) + else: + replaced_conf = get_replaced_config_dict(new_conf, exist_conf, test_keys) + + return replaced_conf + + +def get_replaced_config_dict(new_conf, exist_conf, test_keys=None, key_set=None): + + replaced_conf = dict() + + if test_keys is None: + test_keys = [] + if key_set is None: + key_set = [] + + if not new_conf: + return replaced_conf + + new_key_set = set(new_conf.keys()) + exist_key_set = set(exist_conf.keys()) + + trival_new_key_set = set() + dict_list_new_key_set = set() + for key in new_key_set: + if new_conf[key] not in [None, [], {}]: + if isinstance(new_conf[key], (list, dict)): + dict_list_new_key_set.add(key) + else: + trival_new_key_set.add(key) + + trival_exist_key_set = set() + dict_list_exist_key_set = set() + for key in exist_key_set: + if exist_conf[key] not in [None, [], {}]: + if isinstance(exist_conf[key], (list, dict)): + dict_list_exist_key_set.add(key) + else: + trival_exist_key_set.add(key) + + common_trival_key_set = trival_new_key_set.intersection(trival_exist_key_set) + common_dict_list_key_set = dict_list_new_key_set.intersection(dict_list_exist_key_set) + + key_matched_cnt = 0 + common_trival_key_matched = True + for key in common_trival_key_set: + if new_conf[key] == exist_conf[key]: + if key in key_set: + key_matched_cnt += 1 + else: + if key not in key_set: + common_trival_key_matched = False + + for key in common_dict_list_key_set: + if new_conf[key] == exist_conf[key]: + if key in key_set: + key_matched_cnt += 1 + + key_matched = (key_matched_cnt == len(key_set)) + if key_matched: + extra_trival_new_key_set = trival_new_key_set - common_trival_key_set + extra_trival_exist_key_set = trival_exist_key_set - common_trival_key_set + if extra_trival_new_key_set or extra_trival_exist_key_set or \ + not common_trival_key_matched: + # Replace whole dict. + replaced_conf = exist_conf + return replaced_conf + else: + replaced_conf = [] + return replaced_conf + + for key in key_set: + common_dict_list_key_set.discard(key) + + replace_whole_dict = False + replace_some_list = False + replace_some_dict = False + for key in common_dict_list_key_set: + + new_value = new_conf[key] + exist_value = exist_conf[key] + + if (isinstance(new_value, list) and isinstance(exist_value, list)): + n_list = new_value + e_list = exist_value + t_keys = next((t_key_item[key] for t_key_item in test_keys if key in t_key_item), None) + t_key_set = set() + if t_keys: + t_key_set = set(t_keys.keys()) + + replaced_list = list() + not_dict_item = False + dict_no_key_item = False + for n_item in n_list: + for e_item in e_list: + if (isinstance(n_item, dict) and isinstance(e_item, dict)): + if t_keys: + remaining_keys = [t_key_item for t_key_item in test_keys if key not in t_key_item] + replaced_dict = get_replaced_config_dict(n_item, e_item, + remaining_keys, t_key_set) + else: + dict_no_key_item = True + break + + if replaced_dict: + replaced_list.append(replaced_dict) + break + else: + not_dict_item = True + break + + if not_dict_item or dict_no_key_item: + break + + if dict_no_key_item: + replaced_list = e_list + + if not_dict_item: + n_set = set(n_list) + e_set = set(e_list) + diff_set = n_set.symmetric_difference(e_set) + if diff_set: + replaced_conf[key] = e_list + replace_some_list = True + + elif replaced_list: + replaced_conf[key] = replaced_list + replace_some_list = True + + elif (isinstance(new_value, dict) and isinstance(exist_value, dict)): + replaced_dict = get_replaced_config_dict(new_conf[key], exist_conf[key], test_keys) + if replaced_dict: + replaced_conf[key] = replaced_dict + replace_some_dict = True + + elif (isinstance(new_value, (list, dict)) or isinstance(exist_value, (list, dict))): + # Replace whole dict. + replaced_conf = exist_conf + replace_whole_dict = True + break + + else: + continue + + if ((replace_some_dict or replace_some_list) and (not replace_whole_dict)): + for key in key_set: + replaced_conf[key] = exist_conf[key] + + return replaced_conf + + +def check_required(module, required_parameters, parameters, options_context=None): + '''This utility is a wrapper for the Ansible "check_required_arguments" + function. The "required_parameters" input list provides a list of + key names that are required in the dictionary specified by "parameters". + The optional "options_context" parameter specifies the context/path + from the top level parent dict to the dict being checked.''' + if required_parameters: + spec = {} + for parameter in required_parameters: + spec[parameter] = {'required': True} + + try: + check_required_arguments(spec, parameters, options_context) + except TypeError as exc: + module.fail_json(msg=str(exc)) + + +def get_ranges_in_list(num_list): + """Returns a generator for list(s) of consecutive numbers + present in the given sorted list of numbers + """ + for key, group in groupby(num_list, lambda num, i=count(): num - next(i)): + yield list(group) diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_aaa.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_aaa.py index ddc71331f..c17c0f711 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_aaa.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_aaa.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2021 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -79,8 +79,10 @@ options: - 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. + - In case of replaced, the existing aaa configuration will be replaced with provided configuration. + - In case of overridden, the existing aaa configuration will be overridden with the provided configuration. default: merged - choices: ['merged', 'deleted'] + choices: ['merged', 'deleted', 'overridden', 'replaced'] type: str """ EXAMPLES = """ @@ -169,6 +171,65 @@ EXAMPLES = """ # login-method : local +# Using replaced +# +# Before state: +# ------------- +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : False +# login-method : local, radius + +- name: Replace aaa configurations + dellemc.enterprise_sonic.sonic_aaa: + config: + authentication: + data: + group: ldap + fail_through: true + state: replaced + +# After state: +# ------------ +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : True +# login-method : local, ldap + + +# Using overridden +# +# Before state: +# ------------- +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : False +# login-method : local, radius + +- name: Override aaa configurations + dellemc.enterprise_sonic.sonic_aaa: + config: + authentication: + data: + group: tacacs+ + fail_through: true + state: overridden + +# After state: +# ------------ +# +# do show aaa +# AAA Authentication Information +# --------------------------------------------------------- +# failthrough : True +# login-method : tacacs+ + """ RETURN = """ before: @@ -185,6 +246,13 @@ after: sample: > The configuration returned will always be in the same format of the parameters above. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_acl_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_acl_interfaces.py new file mode 100644 index 000000000..883252bc8 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_acl_interfaces.py @@ -0,0 +1,385 @@ +#!/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_acl_interfaces +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_acl_interfaces +version_added: '2.1.0' +notes: + - Supports C(check_mode). +short_description: Manage access control list (ACL) to interface binding on SONiC +description: + - This module provides configuration management of applying access control lists (ACL) + to interfaces in devices running SONiC. + - ACL needs to be created earlier in the device. +author: 'Arun Saravanan Balachandran (@ArunSaravananBalachandran)' +options: + config: + description: + - Specifies interface access-group configurations. + type: list + elements: dict + suboptions: + name: + description: + - Full name of the interface, i.e. Eth1/1. + type: str + required: true + access_groups: + description: + - Access-group configurations to be set for the interface. + type: list + elements: dict + suboptions: + type: + description: + - Type of the ACLs to be applied on the interface. + type: str + required: true + choices: + - mac + - ipv4 + - ipv6 + acls: + description: + - List of ACLs for the given type. + type: list + elements: dict + suboptions: + name: + description: + - Name of the ACL to be applied on the interface. + type: str + required: true + direction: + description: + - Specifies the direction of the packets that the ACL will be applied on. + type: str + required: true + choices: + - in + - out + state: + description: + - The state of the configuration after module completion. + - I(merged) - Merges provided interface access-group configuration with on-device configuration. + - I(replaced) - Replaces on-device access-group configuration of the specified interfaces with provided configuration. + - I(overridden) - Overrides all on-device interface access-group configurations with the provided configuration. + - I(deleted) - Deletes on-device interface access-group configuration. + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# Using merged +# +# Before State: +# ------------- +# +# sonic# show mac access-group +# sonic# +# sonic# show ip access-group +# sonic# +# sonic# show ipv6 access-group +# Ingress IPV6 access-list ipv6-acl-1 on Eth1/1 +# sonic# + + - name: Merge provided interface access-group configurations + dellemc.enterprise_sonic.sonic_acl_interfaces: + config: + - name: 'Eth1/1' + access_groups: + - type: 'mac' + acls: + - name: 'mac-acl-1' + direction: 'in' + - name: 'mac-acl-2' + direction: 'out' + - type: 'ipv6' + acls: + - name: 'ipv6-acl-2' + direction: 'out' + - name: 'Eth1/2' + access_groups: + - type: 'ipv4' + acls: + - name: 'ip-acl-1' + direction: 'in' + state: merged + +# After State: +# ------------ +# +# sonic# show mac access-group +# Ingress MAC access-list mac-acl-1 on Eth1/1 +# Egress MAC access-list mac-acl-2 on Eth1/1 +# sonic# +# sonic# show ip access-group +# Ingress IP access-list ip-acl-1 on Eth1/2 +# sonic# +# sonic# show ipv6 access-group +# Ingress IPV6 access-list ipv6-acl-1 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/1 +# sonic# + + +# Using replaced +# +# Before State: +# ------------- +# +# sonic# show mac access-group +# Ingress MAC access-list mac-acl-1 on Eth1/1 +# Egress MAC access-list mac-acl-2 on Eth1/1 +# sonic# +# sonic# show ip access-group +# Ingress IP access-list ip-acl-1 on Eth1/2 +# sonic# +# sonic# show ipv6 access-group +# Ingress IPV6 access-list ipv6-acl-1 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/1 +# sonic# + + - name: Replace device access-group configuration of specified interfaces with provided configuration + dellemc.enterprise_sonic.sonic_acl_interfaces: + config: + - name: 'Eth1/2' + access_groups: + - type: 'ipv6' + acls: + - name: 'ipv6-acl-2' + direction: 'out' + - name: 'Eth1/3' + access_groups: + - type: 'ipv4' + acls: + - name: 'ip-acl-2' + direction: 'out' + state: replaced + +# After State: +# ------------ +# +# sonic# show mac access-group +# Ingress MAC access-list mac-acl-1 on Eth1/1 +# Egress MAC access-list mac-acl-2 on Eth1/1 +# sonic# +# sonic# show ip access-group +# Egress IP access-list ip-acl-2 on Eth1/3 +# sonic# +# sonic# show ipv6 access-group +# Ingress IPV6 access-list ipv6-acl-1 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/2 +# sonic# + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show mac access-group +# Ingress MAC access-list mac-acl-1 on Eth1/1 +# Egress MAC access-list mac-acl-2 on Eth1/1 +# sonic# +# sonic# show ip access-group +# Egress IP access-list ip-acl-2 on Eth1/3 +# sonic# +# sonic# show ipv6 access-group +# Ingress IPV6 access-list ipv6-acl-1 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/2 +# sonic# + + - name: Override all interfaces access-group device configuration with provided configuration + dellemc.enterprise_sonic.sonic_acl_interfaces: + config: + - name: 'Eth1/1' + access_groups: + - type: 'ip' + acls: + - name: 'ip-acl-2' + direction: 'out' + - name: 'Eth1/2' + access_groups: + - type: 'ip' + acls: + - name: 'ip-acl-2' + direction: 'out' + state: overridden + +# After State: +# ------------ +# +# sonic# show mac access-group +# sonic# +# sonic# show ip access-group +# Egress IP access-list ip-acl-2 on Eth1/1 +# Egress IP access-list ip-acl-2 on Eth1/2 +# sonic# +# sonic# show ipv6 access-group +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show mac access-group +# Ingress MAC access-list mac-acl-1 on Eth1/1 +# Egress MAC access-list mac-acl-2 on Eth1/1 +# sonic# +# sonic# show ip access-group +# Egress IP access-list ip-acl-2 on Eth1/3 +# sonic# +# sonic# show ipv6 access-group +# Ingress IPV6 access-list ipv6-acl-1 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/2 +# sonic# + + - name: Delete specified interfaces access-group configurations + dellemc.enterprise_sonic.sonic_l2_acls: + config: + - name: 'Eth1/1' + access_groups: + - type: 'mac' + acls: + - name: 'mac-acl-1' + direction: 'in' + - type: 'ipv6' + - name: 'Eth1/2' + state: deleted + +# After State: +# ------------ +# +# sonic# show mac access-group +# Egress MAC access-list mac-acl-2 on Eth1/1 +# sonic# +# sonic# show ip access-group +# Egress IP access-list ip-acl-2 on Eth1/3 +# sonic# +# sonic# show ipv6 access-group +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show mac access-group +# Ingress MAC access-list mac-acl-1 on Eth1/1 +# Egress MAC access-list mac-acl-2 on Eth1/1 +# sonic# +# sonic# show ip access-group +# Egress IP access-list ip-acl-2 on Eth1/3 +# sonic# +# sonic# show ipv6 access-group +# Ingress IPV6 access-list ipv6-acl-1 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/1 +# Egress IPV6 access-list ipv6-acl-2 on Eth1/2 +# sonic# + + - name: Delete all interface access-group configurations + dellemc.enterprise_sonic.sonic_acl_interfaces: + config: + state: deleted + +# After State: +# ------------ +# +# sonic# show mac access-group +# sonic# +# sonic# show ip access-group +# sonic# +# sonic# show ipv6 access-group +# sonic# + + +""" +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. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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.acl_interfaces.acl_interfaces import Acl_interfacesArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.acl_interfaces.acl_interfaces import Acl_interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Acl_interfacesArgs.argument_spec, + supports_check_mode=True) + + result = Acl_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bfd.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bfd.py new file mode 100644 index 000000000..c969b1a69 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bfd.py @@ -0,0 +1,684 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 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_bfd +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: sonic_bfd +version_added: "2.1.0" +short_description: Manage BFD configuration on SONiC +description: + - This module provides configuration management of BFD for devices running SONiC +author: "Shade Talabi (@stalabi1)" +options: + config: + description: + - Specifies BFD configurations + type: dict + suboptions: + profiles: + description: + - List of preconfiguration profiles + type: list + elements: dict + suboptions: + profile_name: + description: + - BFD profile name + type: str + required: True + enabled: + description: + - Enables BFD session when set to true + type: bool + default: True + transmit_interval: + description: + - Specifies peer transmit interval + type: int + default: 300 + receive_interval: + description: + - Specifies peer receive interval + type: int + default: 300 + detect_multiplier: + description: + - Number of missed packets to bring down a BFD session + type: int + default: 3 + passive_mode: + description: + - Specifies BFD peer as passive when set to true + type: bool + default: False + min_ttl: + description: + - Minimum expected TTL on received packets + type: int + default: 254 + echo_interval: + description: + - Specifies echo interval + type: int + default: 300 + echo_mode: + description: + - Echo mode is enabled when set to true + type: bool + default: False + single_hops: + description: + - List of single-hop sessions + type: list + elements: dict + suboptions: + remote_address: + description: + - IP address used by the remote system for the BFD session + type: str + required: True + vrf: + description: + - Name of the configured VRF on the device + type: str + required: True + interface: + description: + - Interface to use to contact peer + type: str + required: True + local_address: + description: + - Source IP address to be used for BFD sessions over the interface + type: str + required: True + enabled: + description: + - Enables BFD session when set to true + type: bool + default: True + transmit_interval: + description: + - Specifies peer transmit interval + type: int + default: 300 + receive_interval: + description: + - Specifies peer receive interval + type: int + default: 300 + detect_multiplier: + description: + - Number of missed packets to bring down a BFD session + type: int + default: 3 + passive_mode: + description: + - Specifies BFD peer as passive when set to true + type: bool + default: False + echo_interval: + description: + - Specifies echo interval + type: int + default: 300 + echo_mode: + description: + - Echo mode is enabled when set to true + type: bool + default: False + profile_name: + description: + - BFD profile name + type: str + multi_hops: + description: + - List of multi-hop sessions + type: list + elements: dict + suboptions: + remote_address: + description: + - IP address used by the remote system for the BFD session + type: str + required: True + vrf: + description: + - Name of the configured VRF on the device + type: str + required: True + local_address: + description: + - Source IP address to be used for BFD sessions over the interface + type: str + required: True + enabled: + description: + - Enables BFD session when set to true + type: bool + default: True + transmit_interval: + description: + - Specifies peer transmit interval + type: int + default: 300 + receive_interval: + description: + - Specifies peer receive interval + type: int + default: 300 + detect_multiplier: + description: + - Number of missed packets to bring down a BFD session + type: int + default: 3 + passive_mode: + description: + - Specifies BFD peer as passive when set to true + type: bool + default: False + min_ttl: + description: + - Minimum expected TTL on received packets + type: int + default: 254 + profile_name: + description: + - BFD profile name + type: str + state: + description: + - The state of the configuration after module completion. + type: str + choices: ['merged', 'deleted', 'replaced', 'overridden'] + default: merged +""" +EXAMPLES = """ +# Using Merged +# +# Before state: +# ------------- +# +# sonic# show bfd profile +# (No "bfd profile" configuration present) +# sonic# show bfd peers +# (No "bfd peers" configuration present) + + - name: Merge BFD configuration + dellemc.enterprise_sonic.sonic_bfd: + config: + profiles: + - profile_name: 'p1' + enabled: True + transmit_interval: 120 + receive_interval: 200 + detect_multiplier: 2 + passive_mode: True + min_ttl: 140 + echo_interval: 150 + echo_mode: True + single_hops: + - remote_address: '196.88.6.1' + vrf: 'default' + interface: 'Ethernet20' + local_address: '1.1.1.1' + enabled: True + transmit_interval: 50 + receive_interval: 80 + detect_multiplier: 4 + passive_mode: True + echo_interval: 110 + echo_mode: True + profile_name: 'p1' + multi_hops: + - remote_address: '192.40.1.3' + vrf: 'default' + local_address: '3.3.3.3' + enabled: True + transmit_interval: 75 + receive_interval: 100 + detect_multiplier: 3 + passive_mode: True + min_ttl: 125 + profile_name: 'p1' + state: merged + +# After state: +# ------------ +# +# sonic# show bfd profile +# BFD Profile: +# Profile-name: p1 +# Enabled: True +# Echo-mode: Enabled +# Passive-mode: Enabled +# Minimum-Ttl: 140 +# Detect-multiplier: 2 +# Receive interval: 200ms +# Transmission interval: 120ms +# Echo transmission interval: 150ms +# sonic# show bfd peers +# BFD Peers: +# +# peer 192.40.1.3 multihop local-address 3.3.3.3 vrf default +# ID: 989720421 +# Remote ID: 0 +# Passive mode: Enabled +# Profile: p1 +# Minimum TTL: 125 +# Status: down +# Downtime: 0 day(s), 0 hour(s), 1 min(s), 46 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 2 +# Receive interval: 100ms +# Transmission interval: 75ms +# Echo transmission interval: ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms +# +# peer 196.88.6.1 local-address 1.1.1.1 vrf default interface Ethernet20 +# ID: 1134635660 +# Remote ID: 0 +# Passive mode: Enabled +# Profile: p1 +# Status: down +# Downtime: 0 day(s), 1 hour(s), 50 min(s), 48 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 4 +# Receive interval: 80ms +# Transmission interval: 50ms +# Echo transmission interval: 110ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms +# +# +# Using replaced +# +# Before state: +# ------------- +# +# sonic# show bfd profile +# BFD Profile: +# Profile-name: p1 +# Enabled: True +# Echo-mode: Enabled +# Passive-mode: Enabled +# Minimum-Ttl: 140 +# Detect-multiplier: 2 +# Receive interval: 200ms +# Transmission interval: 120ms +# Echo transmission interval: 150ms +# Profile-name: p2 +# Enabled: True +# Echo-mode: Disabled +# Passive-mode: Disabled +# Minimum-Ttl: 254 +# Detect-multiplier: 3 +# Receive interval: 300ms +# Transmission interval: 300ms +# Echo transmission interval: 300ms + + - name: Replace BFD configuration + dellemc.enterprise_sonic.sonic_bfd: + config: + profiles: + - profile_name: 'p1' + transmit_interval: 144 + - profile_name: 'p2' + enabled: False + transmit_interval: 110 + receive_interval: 235 + detect_multiplier: 5 + passive_mode: True + min_ttl: 155 + echo_interval: 163 + echo_mode: True + state: replaced + +# After state: +# ------------ +# +# sonic# show bfd profile +# BFD Profile: +# Profile-name: p1 +# Enabled: True +# Echo-mode: Enabled +# Passive-mode: Enabled +# Minimum-Ttl: 140 +# Detect-multiplier: 2 +# Receive interval: 200ms +# Transmission interval: 144ms +# Echo transmission interval: 150ms +# Profile-name: p2 +# Enabled: False +# Echo-mode: Enabled +# Passive-mode: Enabled +# Minimum-Ttl: 155 +# Detect-multiplier: 5 +# Receive interval: 235ms +# Transmission interval: 110ms +# Echo transmission interval: 163ms +# +# +# Using overridden +# +# Before state: +# ------------- +# +# sonic# show bfd peers +# BFD Peers: +# +# peer 192.40.1.3 multihop local-address 3.3.3.3 vrf default +# ID: 989720421 +# Remote ID: 0 +# Passive mode: Enabled +# Profile: p1 +# Minimum TTL: 125 +# Status: down +# Downtime: 0 day(s), 0 hour(s), 1 min(s), 46 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 2 +# Receive interval: 100ms +# Transmission interval: 75ms +# Echo transmission interval: ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms +# +# peer 196.88.6.1 local-address 1.1.1.1 vrf default interface Ethernet20 +# ID: 1134635660 +# Remote ID: 0 +# Passive mode: Enabled +# Profile: p1 +# Status: down +# Downtime: 0 day(s), 1 hour(s), 50 min(s), 48 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 4 +# Receive interval: 80ms +# Transmission interval: 50ms +# Echo transmission interval: 110ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms + + - name: Override BFD configuration + dellemc.enterprise_sonic.sonic_bfd: + config: + single_hops: + - remote_address: '172.68.2.1' + vrf: 'default' + interface: 'Ethernet16' + local_address: '2.2.2.2' + enabled: True + transmit_interval: 60 + receive_interval: 88 + detect_multiplier: 6 + passive_mode: True + echo_interval: 112 + echo_mode: True + profile_name: 'p3' + multi_hops: + - remote_address: '186.42.1.2' + vrf: 'default' + local_address: '1.1.1.1' + enabled: False + transmit_interval: 85 + receive_interval: 122 + detect_multiplier: 4 + passive_mode: False + min_ttl: 120 + profile_name: 'p3' + state: overridden + +# After state: +# ------------ +# +# sonic# show bfd peers +# BFD Peers: +# +# peer 186.42.1.2 multihop local-address 1.1.1.1 vrf default +# ID: 989720421 +# Remote ID: 0 +# Passive mode: Disabled +# Profile: p3 +# Minimum TTL: 120 +# Status: down +# Downtime: 0 day(s), 0 hour(s), 1 min(s), 46 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 4 +# Receive interval: 122ms +# Transmission interval: 85ms +# Echo transmission interval: ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms +# +# peer 172.68.2.1 local-address 2.2.2.2 vrf default interface Ethernet16 +# ID: 1134635660 +# Remote ID: 0 +# Passive mode: Enabled +# Profile: p3 +# Status: down +# Downtime: 0 day(s), 1 hour(s), 50 min(s), 48 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 6 +# Receive interval: 88ms +# Transmission interval: 60ms +# Echo transmission interval: 112ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms +# +# +# Using deleted +# +# Before state: +# ------------- +# +# sonic# show bfd profile +# BFD Profile: +# Profile-name: p1 +# Enabled: True +# Echo-mode: Enabled +# Passive-mode: Enabled +# Minimum-Ttl: 140 +# Detect-multiplier: 2 +# Receive interval: 200ms +# Transmission interval: 120ms +# Echo transmission interval: 150ms +# sonic# show bfd peers +# BFD Peers: +# +# peer 192.40.1.3 multihop local-address 3.3.3.3 vrf default +# ID: 989720421 +# Remote ID: 0 +# Passive mode: Enabled +# Profile: p1 +# Minimum TTL: 125 +# Status: down +# Downtime: 0 day(s), 0 hour(s), 1 min(s), 46 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 2 +# Receive interval: 100ms +# Transmission interval: 75ms +# Echo transmission interval: ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms +# +# peer 196.88.6.1 local-address 1.1.1.1 vrf default interface Ethernet20 +# ID: 1134635660 +# Remote ID: 0 +# Passive mode: Enabled +# Profile: p1 +# Status: down +# Downtime: 0 day(s), 1 hour(s), 50 min(s), 48 sec(s) +# Diagnostics: ok +# Remote diagnostics: ok +# Peer Type: configured +# Local timers: +# Detect-multiplier: 4 +# Receive interval: 80ms +# Transmission interval: 50ms +# Echo transmission interval: 110ms +# Remote timers: +# Detect-multiplier: 3 +# Receive interval: 1000ms +# Transmission interval: 1000ms +# Echo transmission interval: 0ms + + - name: Delete BFD configuration + dellemc.enterprise_sonic.sonic_bfd: + config: + profiles: + - profile_name: 'p1' + enabled: True + transmit_interval: 120 + receive_interval: 200 + detect_multiplier: 2 + passive_mode: True + min_ttl: 140 + echo_interval: 150 + echo_mode: True + single_hops: + - remote_address: '196.88.6.1' + vrf: 'default' + interface: 'Ethernet20' + local_address: '1.1.1.1' + multi_hops: + - remote_address: '192.40.1.3' + vrf: 'default' + local_address: '3.3.3.3' + state: deleted + +# After state +# ----------- +# +# sonic# show bfd profile +# BFD Profile: +# Profile-name: p1 +# Enabled: True +# Echo-mode: Disabled +# Passive-mode: Disabled +# Minimum-Ttl: 254 +# Detect-multiplier: 3 +# Receive interval: 300ms +# Transmission interval: 300ms +# Echo transmission interval: 300ms +# sonic# show bfd peers +# (No "bfd peers" configuration present) +""" +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.bfd.bfd import BfdArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.bfd.bfd import Bfd + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=BfdArgs.argument_spec, + supports_check_mode=True) + + result = Bfd(module).execute_module() + 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 index bc53ca40c..aaf52a40c 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# © Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# © Copyright 2023 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) @@ -143,13 +143,20 @@ options: description: - Allows comparing meds from different neighbors if set to true type: bool + rt_delay: + description: + - Time in seconds to wait before processing route-map changes. + - Range is 0-600. 0 disables the timer and changes to route-map will not be updated. + type: int 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. + - In case of replaced, the existing configuration of the specified BGP AS will be replaced with provided configuration. + - In case of overridden, the existing BGP configuration will be overridden with the provided configuration. default: merged - choices: ['merged', 'deleted'] + choices: ['merged', 'deleted', 'replaced', 'overridden'] type: str """ EXAMPLES = """ @@ -158,30 +165,33 @@ EXAMPLES = """ # 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 -#! +# ! +# router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# route-map delay-timer 20 +# 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 +# route-map delay-timer 10 +# 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 + rt_delay: 10 log_neighbor_changes: False bestpath: as_path: @@ -195,6 +205,7 @@ EXAMPLES = """ missing_as_worst: True - bgp_as: 10 router_id: 10.2.2.32 + rt_delay: 20 log_neighbor_changes: True vrf_name: 'VrfCheck1' - bgp_as: 11 @@ -215,18 +226,18 @@ EXAMPLES = """ # 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 -#! +# ! +# 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 @@ -234,24 +245,26 @@ EXAMPLES = """ # 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 -#! +# ! +# router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# route-map delay-timer 20 +# 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 +# route-map delay-timer 10 +# 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: @@ -261,8 +274,8 @@ EXAMPLES = """ # After state: # ------------ # -#! -#! +# ! +# ! # Using merged @@ -270,16 +283,17 @@ EXAMPLES = """ # Before state: # ------------- # -#! -#router bgp 4 -# router-id 10.1.1.4 -#! +# ! +# 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 + rt_delay: 10 log_neighbor_changes: False timers: holdtime: 20 @@ -301,6 +315,7 @@ EXAMPLES = """ med_val: 7878 - bgp_as: 10 router_id: 10.2.2.32 + rt_delay: 20 log_neighbor_changes: True vrf_name: 'VrfCheck1' - bgp_as: 11 @@ -320,28 +335,172 @@ EXAMPLES = """ # After state: # ------------ # +# ! +# router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# route-map delay-timer 20 +# 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 +# route-map delay-timer 10 +# 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 +# +# ! + + +# Using replaced +# +# Before state: +# ------------- +# +#! +#router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# log-neighbor-changes +# timers 60 180 +#! +#router bgp 4 +# router-id 10.2.2.4 +# max-med on-startup 667 7878 +# bestpath as-path ignore +# bestpath as-path confed +# bestpath med missing-as-worst confed +# bestpath compare-routerid +# timers 20 30 +#! +# + +- name: Replace device configuration of specified BGP AS with provided + dellemc.enterprise_sonic.sonic_bgp: + config: + - bgp_as: 4 + router_id: 10.2.2.44 + log_neighbor_changes: True + bestpath: + as_path: + confed: True + compare_routerid: True + - bgp_as: 11 + vrf_name: 'VrfCheck2' + router_id: 10.2.2.33 + log_neighbor_changes: True + bestpath: + as_path: + confed: True + ignore: True + compare_routerid: True + med: + confed: True + missing_as_worst: True + state: replaced + +# +# After state: +# ------------ +# #! #router bgp 10 vrf VrfCheck1 # router-id 10.2.2.32 # log-neighbor-changes +# timers 60 180 #! #router bgp 11 vrf VrfCheck2 +# router-id 10.2.2.33 # log-neighbor-changes # bestpath as-path ignore +# bestpath as-path confed # bestpath med missing-as-worst confed # bestpath compare-routerid +# timers 60 180 +#! +#router bgp 4 +# router-id 10.2.2.44 +# log-neighbor-changes +# bestpath as-path confed +# bestpath compare-routerid +# timers 60 180 +#! + + +# Using overridden +# +# Before state: +# ------------- +# +#! +#router bgp 10 vrf VrfCheck1 +# router-id 10.2.2.32 +# log-neighbor-changes +# timers 60 180 #! #router bgp 4 # router-id 10.2.2.4 +# max-med on-startup 667 7878 # 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 +#! +# + +- name: Override device configuration of global BGP with provided configuration + dellemc.enterprise_sonic.sonic_bgp: + config: + - bgp_as: 4 + router_id: 10.2.2.44 + log_neighbor_changes: True + bestpath: + as_path: + confed: True + compare_routerid: True + - bgp_as: 11 + vrf_name: 'VrfCheck2' + router_id: 10.2.2.33 + log_neighbor_changes: True + bestpath: + as_path: + confed: True + ignore: True + compare_routerid: True + timers: + holdtime: 90 + keepalive_interval: 30 + state: overridden + +# +# After state: +# ------------ # #! +#router bgp 11 vrf VrfCheck2 +# router-id 10.2.2.33 +# log-neighbor-changes +# bestpath as-path ignore +# bestpath as-path confed +# bestpath compare-routerid +# timers 30 90 +#! +#router bgp 4 +# router-id 10.2.2.44 +# log-neighbor-changes +# bestpath as-path confed +# bestpath compare-routerid +# timers 60 180 +#! """ 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 index 6d55355c9..af00093c6 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_af.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_af.py @@ -172,13 +172,62 @@ options: description: - Specifies the count of the ebgp multipaths count. type: int + rd: + description: + - Specifies the route distiguisher to be used by the VRF instance. + type: str + rt_in: + description: + - Route-targets to be imported. + type: list + elements: str + rt_out: + description: + - Route-targets to be exported. + type: list + elements: str + vnis: + description: + - VNI configuration for the EVPN. + type: list + elements: dict + suboptions: + vni_number: + description: + - Specifies the VNI number. + type: int + required: True + advertise_default_gw: + description: + - Specifies the advertise default gateway flag. + type: bool + advertise_svi_ip: + description: + - Enables advertise SVI MACIP routes + type: bool + rd: + description: + - Specifies the route distiguisher to be used by the VRF instance. + type: str + rt_in: + description: + - Route-targets to be imported. + type: list + elements: str + rt_out: + description: + - Route-targets to be exported. + type: list + elements: str 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. + - In case of replaced, the existing BGP_AF of specified BGP AS will be replaced with provided configuration. + - In case of overridden, the existing BGP_AF configuration will be overridden with the provided configuration. default: merged - choices: ['merged', 'deleted'] + choices: ['merged', 'deleted', 'overridden', 'replaced'] type: str """ EXAMPLES = """ @@ -208,8 +257,17 @@ EXAMPLES = """ # address-family l2vpn evpn # advertise-svi-ip # advertise ipv6 unicast route-map aa +# rd 3.3.3.3:33 +# route-target import 22:22 +# route-target export 33:33 # advertise-pip ip 1.1.1.1 peer-ip 2.2.2.2 -#! +# ! +# vni 1 +# advertise-default-gw +# advertise-svi-ip +# rd 5.5.5.5:55 +# route-target import 88:88 +# route-target export 77:77 # - name: Delete BGP Address family configuration from the device dellemc.enterprise_sonic.sonic_bgp_af: @@ -228,6 +286,13 @@ EXAMPLES = """ route_advertise_list: - advertise_afi: ipv6 route_map: aa + rd: "3.3.3.3:33" + rt_in: + - "22:22" + rt_out: + - "33:33" + vnis: + - vni_number: 1 - afi: ipv4 safi: unicast - afi: ipv6 @@ -320,6 +385,20 @@ EXAMPLES = """ route_advertise_list: - advertise_afi: ipv4 route_map: bb + rd: "1.1.1.1:11" + rt_in: + - "12:12" + rt_out: + - "13:13" + vnis: + - vni_number: 1 + advertise_default_gw: True + advertise_svi_ip: True + rd: "5.5.5.5:55" + rt_in: + - "88:88" + rt_out: + - "77:77" - afi: ipv4 safi: unicast network: @@ -366,8 +445,300 @@ EXAMPLES = """ # address-family l2vpn evpn # advertise-svi-ip # advertise ipv4 unicast route-map bb +# rd 1.1.1.1:11 +# route-target import 12:12 +# route-target import 13:13 # advertise-pip ip 3.3.3.3 peer-ip 4.4.4.4 +# ! +# vni 1 +# advertise-default-gw +# advertise-svi-ip +# rd 5.5.5.5:55 +# route-target import 88:88 +# route-target export 77:77 +# + + +# Using replaced # +# Before state: +# ------------- +# +#do show running-configuration bgp +#! +#router bgp 52 vrf VrfReg1 +# log-neighbor-changes +# timers 60 180 +# ! +# address-family ipv4 unicast +# maximum-paths 1 +# maximum-paths ibgp 1 +# network 3.3.3.3/16 +# dampening +#! +#router bgp 51 +# router-id 111.2.2.41 +# log-neighbor-changes +# timers 60 180 +# ! +# address-family ipv4 unicast +# redistribute connected route-map bb metric 21 +# redistribute ospf route-map bb metric 27 +# maximum-paths 1 +# maximum-paths ibgp 1 +# network 2.2.2.2/16 +# network 192.168.10.1/32 +# dampening +# ! +# address-family ipv6 unicast +# redistribute static route-map aa metric 26 +# maximum-paths 4 +# maximum-paths ibgp 5 +# ! +# address-family l2vpn evpn +# advertise-all-vni +# advertise-svi-ip +# advertise ipv4 unicast route-map bb +# rd 1.1.1.1:11 +# route-target import 12:12 +# route-target export 13:13 +# dup-addr-detection +# advertise-pip ip 3.3.3.3 peer-ip 4.4.4.4 +# ! +# vni 1 +# advertise-default-gw +# advertise-svi-ip +# rd 5.5.5.5:55 +# route-target import 88:88 +# route-target export 77:77 + +- name: Replace device configuration of address families of specified BGP AS with provided configuration. + 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 + rd: "1.1.1.1:11" + rt_in: + - "22:22" + rt_out: + - "13:13" + vnis: + - vni_number: 5 + advertise_default_gw: True + advertise_svi_ip: True + rd: "10.10.10.10:55" + rt_in: + - "88:88" + rt_out: + - "77:77" + - afi: ipv4 + safi: unicast + network: + - 2.2.2.2/16 + - 192.168.10.1/32 + dampening: True + redistribute: + - protocol: connected + - protocol: ospf + metric: 30 + state: replaced + +# After state: +# ------------ +# +#do show running-configuration bgp +#! +#router bgp 52 vrf VrfReg1 +# log-neighbor-changes +# timers 60 180 +# ! +# address-family ipv4 unicast +# maximum-paths 1 +# maximum-paths ibgp 1 +# network 3.3.3.3/16 +# dampening +#! +#router bgp 51 +# router-id 111.2.2.41 +# log-neighbor-changes +# timers 60 180 +# ! +# address-family ipv4 unicast +# redistribute connected +# redistribute ospf metric 30 +# maximum-paths 1 +# maximum-paths ibgp 1 +# network 2.2.2.2/16 +# network 192.168.10.1/32 +# dampening +# ! +# address-family l2vpn evpn +# advertise-all-vni +# advertise-svi-ip +# advertise ipv4 unicast route-map bb +# rd 1.1.1.1:11 +# route-target import 22:22 +# route-target export 13:13 +# dup-addr-detection +# advertise-pip ip 3.3.3.3 peer-ip 4.4.4.4 +# ! +# vni 5 +# advertise-default-gw +# advertise-svi-ip +# rd 10.10.10.10:55 +# route-target import 88:88 +# route-target export 77:77 + + +# Using overridden +# +# Before state: +# ------------- +# +#do show running-configuration bgp +#! +#router bgp 52 vrf VrfReg1 +# log-neighbor-changes +# timers 60 180 +# ! +# address-family ipv4 unicast +# maximum-paths 1 +# maximum-paths ibgp 1 +# network 3.3.3.3/16 +# dampening +#! +#router bgp 51 +# router-id 111.2.2.41 +# log-neighbor-changes +# timers 60 180 +# ! +# address-family ipv4 unicast +# redistribute connected route-map bb metric 21 +# redistribute ospf route-map bb metric 27 +# maximum-paths 1 +# maximum-paths ibgp 1 +# network 2.2.2.2/16 +# network 192.168.10.1/32 +# dampening +# ! +# address-family ipv6 unicast +# redistribute static route-map aa metric 26 +# maximum-paths 4 +# maximum-paths ibgp 5 +# ! +# address-family l2vpn evpn +# advertise-all-vni +# advertise-svi-ip +# advertise ipv4 unicast route-map bb +# rd 1.1.1.1:11 +# route-target import 12:12 +# route-target export 13:13 +# dup-addr-detection +# advertise-pip ip 3.3.3.3 peer-ip 4.4.4.4 +# ! +# vni 1 +# advertise-default-gw +# advertise-svi-ip +# rd 5.5.5.5:55 +# route-target import 88:88 +# route-target export 77:77 + +- name: Override device configuration of BGP address families with provided configuration. + 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 + rd: "1.1.1.1:11" + rt_in: + - "22:22" + rt_out: + - "13:13" + vnis: + - vni_number: 5 + advertise_default_gw: True + advertise_svi_ip: True + rd: "10.10.10.10:55" + rt_in: + - "88:88" + rt_out: + - "77:77" + - afi: ipv4 + safi: unicast + network: + - 2.2.2.2/16 + - 192.168.10.1/32 + dampening: True + redistribute: + - protocol: connected + - protocol: ospf + metric: 30 + state: overridden + +# After state: +# ------------ +# +#do show running-configuration bgp +#! +#router bgp 52 vrf VrfReg1 +# log-neighbor-changes +# timers 60 180 +#! +#router bgp 51 +# router-id 111.2.2.41 +# log-neighbor-changes +# timers 60 180 +# ! +# address-family ipv4 unicast +# redistribute connected +# redistribute ospf metric 30 +# maximum-paths 1 +# maximum-paths ibgp 1 +# network 2.2.2.2/16 +# network 192.168.10.1/32 +# dampening +# ! +# address-family l2vpn evpn +# advertise-all-vni +# advertise-svi-ip +# advertise ipv4 unicast route-map bb +# rd 1.1.1.1:11 +# route-target import 22:22 +# route-target export 13:13 +# dup-addr-detection +# advertise-pip ip 3.3.3.3 peer-ip 4.4.4.4 +# ! +# vni 5 +# advertise-default-gw +# advertise-svi-ip +# rd 10.10.10.10:55 +# route-target import 88:88 +# route-target export 77:77 + + """ RETURN = """ before: 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 index bd2ff74a1..9bc3f43f5 100644 --- 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 @@ -62,7 +62,8 @@ options: required: False type: bool description: - - Permits or denies this as path. + - Permits or denies this as-path. + - Default value while adding a new as-path-list is C(False). state: description: - The state of the configuration after module completion. @@ -70,6 +71,8 @@ options: choices: - merged - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -83,21 +86,21 @@ EXAMPLES = """ # 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 + - 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: +# action: permit # members: 808.* @@ -114,12 +117,12 @@ EXAMPLES = """ # action: deny # members: 608.*,709.* -- name: Deletes BGP as-path list - dellemc.enterprise_sonic.sonic_bgp_as_paths: - config: - - name: test - members: - state: deleted + - name: Deletes BGP as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + state: deleted # After state: # ------------ @@ -140,10 +143,10 @@ EXAMPLES = """ # action: permit # members: 808.*,909.* -- name: Deletes BGP as-path list - dellemc.enterprise_sonic.sonic_bgp_as_paths: - config: - state: deleted + - name: Deletes BGP as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + state: deleted # After state: # ------------ @@ -158,16 +161,16 @@ EXAMPLES = """ # ------------- # # show bgp as-path-access-list -# AS path list test: +# (No bgp as-path-access-list configuration present) -- name: Adds 909.* to test as-path list - dellemc.enterprise_sonic.sonic_bgp_as_paths: - config: - - name: test - members: - - 909.* - permit: true - state: merged + - name: Create a BGP as-path list + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + - 909.* + permit: true + state: merged # After state: # ------------ @@ -178,6 +181,78 @@ EXAMPLES = """ # members: 909.* +# Using replaced + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 800.*,808.* +# AS path list test1: +# action: deny +# members: 500.* + + - name: Replace device configuration of specified BGP as-path lists with provided configuration + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + - 900.* + - 901.* + permit: true + - name: test1 + - name: test2 + members: + - 100.* + permit: true + state: replaced + +# After state: +# ------------ +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 900.*,901.* +# AS path list test2: +# action: permit +# members: 100.* + + +# Using overridden + +# Before state: +# ------------- +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 800.*,808.* +# AS path list test1: +# action: deny +# members: 500.* + + - name: Override device configuration of all BGP as-path lists with provided configuration + dellemc.enterprise_sonic.sonic_bgp_as_paths: + config: + - name: test + members: + - 900.* + - 901.* + permit: true + state: overridden + +# After state: +# ------------ +# +# show bgp as-path-access-list +# AS path list test: +# action: permit +# members: 900.*,901.* + + """ RETURN = """ before: 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 index 08c8dcc7f..dd1c2b083 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_communities.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_communities.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -52,7 +52,7 @@ options: required: True type: str description: - - Name of the BGP communitylist. + - Name of the BGP community-list. type: type: str description: @@ -67,6 +67,7 @@ options: type: bool description: - Permits or denies this community. + - Default value while adding a new community-list is C(False). aann: required: False type: str @@ -120,6 +121,8 @@ options: choices: - merged - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -130,18 +133,21 @@ EXAMPLES = """ # # show bgp community-list # Standard community list test: match: ANY -# 101 -# 201 -# Standard community list test1: match: ANY -# 301 +# permit local-as +# permit no-peer +# Expanded community list test1: match: ANY +# deny 101 +# deny 302 -- name: Deletes BGP community member +- name: Delete a BGP community-list member dellemc.enterprise_sonic.sonic_bgp_communities: config: - - name: test + - name: test1 + type: expanded + permit: false members: regex: - - 201 + - 302 state: deleted # After state: @@ -149,9 +155,10 @@ EXAMPLES = """ # # show bgp community-list # Standard community list test: match: ANY -# 101 -# Standard community list test1: match: ANY -# 301 +# permit local-as +# permit no-peer +# Expanded community list test1: match: ANY +# deny 101 # Using deleted @@ -161,15 +168,17 @@ EXAMPLES = """ # # show bgp community-list # Standard community list test: match: ANY -# 101 +# permit local-as +# permit no-peer # Expanded community list test1: match: ANY -# 201 +# deny 101 +# deny 302 -- name: Deletes a single BGP community +- name: Delete a single BGP community-list dellemc.enterprise_sonic.sonic_bgp_communities: config: - name: test - members: + type: standard state: deleted # After state: @@ -177,7 +186,8 @@ EXAMPLES = """ # # show bgp community-list # Expanded community list test1: match: ANY -# 201 +# deny 101 +# deny 302 # Using deleted @@ -187,11 +197,13 @@ EXAMPLES = """ # # show bgp community-list # Standard community list test: match: ANY -# 101 +# permit local-as +# permit no-peer # Expanded community list test1: match: ANY -# 201 +# deny 101 +# deny 302 -- name: Delete All BGP communities +- name: Delete All BGP community-lists dellemc.enterprise_sonic.sonic_bgp_communities: config: state: deleted @@ -210,14 +222,17 @@ EXAMPLES = """ # # show bgp community-list # Standard community list test: match: ANY -# 101 +# permit local-as +# permit no-peer # Expanded community list test1: match: ANY -# 201 +# deny 101 +# deny 302 -- name: Deletes all members in a single BGP community +- name: Delete all members in a single BGP community-list dellemc.enterprise_sonic.sonic_bgp_communities: config: - - name: test + - name: test1 + type: expanded members: regex: state: deleted @@ -226,9 +241,9 @@ EXAMPLES = """ # ------------ # # show bgp community-list -# Expanded community list test: match: ANY -# Expanded community list test1: match: ANY -# 201 +# Standard community list test: match: ANY +# permit local-as +# permit no-peer # Using merged @@ -236,23 +251,105 @@ EXAMPLES = """ # Before state: # ------------- # -# show bgp as-path-access-list -# AS path list test: +# show bgp community-list +# Expanded community list test1: match: ANY +# permit 101 +# permit 302 -- name: Adds 909.* to test as-path list - dellemc.enterprise_sonic.sonic_bgp_as_paths: +- name: Add a new BGP community-list + dellemc.enterprise_sonic.sonic_bgp_communities: config: - - name: test + - name: test2 + type: expanded + permit: true members: - - 909.* + regex: + - 909 state: merged # After state: # ------------ # -# show bgp as-path-access-list -# AS path list test: -# members: 909.* +# show bgp community-list +# Expanded community list test1: match: ANY +# permit 101 +# permit 302 +# Expanded community list test2: match: ANY +# permit 909 + + +# Using replaced + +# Before state: +# ------------- +# +# show bgp community-list +# Standard community list test: match: ANY +# permit local-as +# permit no-peer +# Expanded community list test1: match: ANY +# deny 101 +# deny 302 + +- name: Replacing a single BGP community-list + dellemc.enterprise_sonic.sonic_bgp_communities: + config: + - name: test + type: expanded + members: + regex: + - 301 + - name: test3 + type: standard + no_advertise: true + no_peer: true + permit: false + match: ALL + state: replaced + +# After state: +# ------------ +# +# show bgp community-list +# Expanded community list test: match: ANY +# deny 301 +# Expanded community list test1: match: ANY +# deny 101 +# deny 302 +# Standard community list test3: match: ALL +# deny no-advertise +# deny no-peer + + +# Using overridden + +# Before state: +# ------------- +# +# show bgp community-list +# Standard community list test: match: ANY +# permit local-as +# permit no-peer +# Expanded community list test1: match: ANY +# deny 101 +# deny 302 + +- name: Override entire BGP community-lists + dellemc.enterprise_sonic.sonic_bgp_communities: + config: + - name: test3 + type: expanded + members: + regex: + - 301 + state: overridden + +# After state: +# ------------ +# +# show bgp community-list +# Expanded community list test3: match: ANY +# deny 301 """ 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 index c2af0c488..49a30c9f9 100644 --- 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 @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -66,6 +66,7 @@ options: type: bool description: - Permits or denies this community. + - Default value while adding a new ext-community-list is False. members: required: False type: dict @@ -106,6 +107,8 @@ options: choices: - merged - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -116,15 +119,16 @@ EXAMPLES = """ # # show bgp ext-community-list # Standard extended community list test: match: ANY -# rt:101:101 -# rt:201:201 +# permit rt:101:101 +# permit rt:201:201 - name: Deletes a BGP ext community member dellemc.enterprise_sonic.sonic_bgp_ext_communities: config: - name: test + type: standard members: - regex: + route_target: - 201:201 state: deleted @@ -133,7 +137,7 @@ EXAMPLES = """ # # show bgp ext-community-list # Standard extended community list test: match: ANY -# rt:101:101 +# permit rt:101:101 # @@ -144,9 +148,10 @@ EXAMPLES = """ # # show bgp ext-community-list # Standard extended community list test: match: ANY -# 101 -# Expanded extended community list test1: match: ANY -# 201 +# permit rt:101:101 +# permit rt:201:201 +# Expanded extended community list test1: match: ALL +# deny 101:102 - name: Deletes a single BGP extended community dellemc.enterprise_sonic.sonic_bgp_ext_communities: @@ -160,7 +165,8 @@ EXAMPLES = """ # # show bgp ext-community-list # Standard extended community list test: match: ANY -# 101 +# permit rt:101:101 +# permit rt:201:201 # @@ -171,9 +177,10 @@ EXAMPLES = """ # # show bgp ext-community-list # Standard extended community list test: match: ANY -# 101 -# Expanded extended community list test1: match: ANY -# 201 +# permit rt:101:101 +# permit rt:201:201 +# Expanded extended community list test1: match: ALL +# deny 101:102 - name: Deletes all BGP extended communities dellemc.enterprise_sonic.sonic_bgp_ext_communities: @@ -194,9 +201,10 @@ EXAMPLES = """ # # show bgp ext-community-list # Standard extended community list test: match: ANY -# 101 -# Expanded extended community list test1: match: ANY -# 201 +# permit rt:101:101 +# permit rt:201:201 +# Expanded extended community list test1: match: ALL +# deny 101:102 - name: Deletes all members in a single BGP extended community dellemc.enterprise_sonic.sonic_bgp_ext_communities: @@ -211,8 +219,8 @@ EXAMPLES = """ # # show bgp ext-community-list # Standard extended community list test: match: ANY -# 101 -# Expanded extended community list test1: match: ANY +# permit rt:101:101 +# permit rt:201:201 # @@ -221,23 +229,108 @@ EXAMPLES = """ # Before state: # ------------- # -# show bgp as-path-access-list -# AS path list test: +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# permit rt:101:101 +# permit rt:201:201 +# Expanded extended community list test1: match: ALL +# deny 101:102 -- name: Adds 909.* to test as-path list - dellemc.enterprise_sonic.sonic_bgp_as_paths: +- name: Adds new community list + dellemc.enterprise_sonic.sonic_bgp_ext_communities: config: - - name: test + - name: test3 + type: standard + match: any + permit: true members: - - 909.* + route_origin: + - "301:301" + - "401:401" state: merged # After state: # ------------ # -# show bgp as-path-access-list -# AS path list test: -# members: 909.* +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# permit rt:101:101 +# permit rt:201:201 +# Expanded extended community list test1: match: ALL +# deny 101:102 +# Standard extended community list test3: match: ANY +# permit soo:301:301 +# permit soo:401:401 + + + +# Using replaced + +# Before state: +# ------------- +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# permit rt:101:101 +# permit rt:201:201 +# Expanded extended community list test1: match: ALL +# deny 101:102 + +- name: Replacing a single BGP extended community + dellemc.enterprise_sonic.sonic_bgp_ext_communities: + config: + - name: test + type: expanded + permit: true + match: all + members: + regex: + - 301:302 + state: replaced + +# After state: +# ------------ +# +# show bgp ext-community-list +# Expanded extended community list test: match: ALL +# permit 301:302 +# Expanded extended community list test1: match: ALL +# deny 101:102 +# + + +# Using overridden + +# Before state: +# ------------- +# +# show bgp ext-community-list +# Standard extended community list test: match: ANY +# permit rt:101:101 +# permit rt:201:201 +# Expanded extended community list test1: match: ALL +# deny 101:102 + + +- name: Override the entire list of BGP extended community + dellemc.enterprise_sonic.sonic_bgp_ext_communities: + config: + - name: test3 + type: expanded + permit: true + match: all + members: + regex: + - 301:302 + state: overridden + +# After state: +# ------------ +# +# show bgp ext-community-list +# Expanded extended community list test3: match: ALL +# permit 301:302 +# """ 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 index 19aeb6fc9..47a414b0a 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_bgp_neighbors.py @@ -296,7 +296,7 @@ options: default: False prefix_limit: description: - - Specifies prefix limit attributes. + - Specifies prefix limit attributes for ipv4-unicast and ipv6-unicast. type: dict suboptions: max_prefixes: 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 index 10400cfe2..d3b23dfb2 100644 --- 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 @@ -127,7 +127,7 @@ options: default: False prefix_limit: description: - - Specifies prefix limit attributes. + - Specifies prefix limit attributes for ipv4-unicast and ipv6-unicast. type: dict suboptions: max_prefixes: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_config.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_config.py index dd054419f..96c0ee1ba 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_config.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_config.py @@ -318,7 +318,7 @@ def main(): if module.params['save']: result['changed'] = True if not module.check_mode: - cmd = {r'command': ' write memory'} + cmd = {r'command': 'write memory'} run_commands(module, [cmd]) result['saved'] = True diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_copp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_copp.py new file mode 100644 index 000000000..e4e7d358a --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_copp.py @@ -0,0 +1,295 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 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_copp +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: sonic_copp +version_added: "2.1.0" +short_description: Manage CoPP configuration on SONiC +description: + - This module provides configuration management of CoPP for devices running SONiC +author: "Shade Talabi (@stalabi1)" +options: + config: + description: + - Specifies CoPP configurations + type: dict + suboptions: + copp_groups: + description: + - List of CoPP entries that comprise a CoPP group + type: list + elements: dict + suboptions: + copp_name: + description: + - Name of CoPP classifier + type: str + required: True + trap_priority: + description: + - CoPP trap priority + type: int + trap_action: + description: + - CoPP trap action + type: str + queue: + description: + - CoPP queue ID + type: int + cir: + description: + - Committed information rate in bps or pps (packets per second) + type: str + cbs: + description: + - Committed bucket size in packets or bytes + type: str + state: + description: + - The state of the configuration after module completion + type: str + choices: ['merged', 'deleted', 'replaced', 'overridden'] + default: merged +""" +EXAMPLES = """ +# Using merged +# +# Before state: +# ------------- +# +# sonic# show copp actions +# (No "copp actions" configuration present) + + - name: Merge CoPP groups configuration + dellemc.enterprise_sonic.sonic_copp: + config: + copp_groups: + - copp_name: 'copp-1' + trap_priority: 1 + trap_action: 'DROP' + queue: 1 + cir: '45' + cbs: '45' + - copp_name: 'copp-2' + trap_priority: 2 + trap_action: 'FORWARD' + queue: 2 + cir: '90' + cbs: '90' + state: merged + +# After state: +# ------------ +# +# sonic# show copp actions +# CoPP action group copp-1 +# trap-action drop +# trap-priority 1 +# trap-queue 1 +# police cir 45 cbs 45 +# CoPP action group copp-2 +# trap-action forward +# trap-priority 2 +# trap-queue 2 +# police cir 90 cbs 90 +# +# +# Using replaced +# +# Before state: +# ------------- +# +# sonic# show copp actions +# CoPP action group copp-1 +# trap-action drop +# trap-priority 1 +# trap-queue 1 +# police cir 45 cbs 45 + + - name: Replace CoPP groups configuration + dellemc.enterprise_sonic.sonic_copp: + config: + copp_groups: + - copp_name: 'copp-1' + trap_priority: 2 + trap_action: 'FORWARD' + queue: 2 + - copp_name: 'copp-3' + trap_priority: 3 + trap_action: 'DROP' + queue: 3 + cir: '1000' + cbs: '1000' + state: replaced + +# After state: +# ------------ +# +# sonic# show copp actions +# CoPP action group copp-1 +# trap-action forward +# trap-priority 2 +# trap-queue 2 +# CoPP action group copp-3 +# trap-action drop +# trap-priority 3 +# trap-queue 3 +# police cir 1000 cbs 1000 +# +# +# Using overridden +# +# Before state: +# ------------- +# +# sonic# show copp actions +# CoPP action group copp-1 +# trap-action forward +# trap-priority 2 +# trap-queue 2 +# CoPP action group copp-3 +# trap-action drop +# trap-priority 3 +# trap-queue 3 +# police cir 1000 cbs 1000 + + - name: Override CoPP groups configuration + dellemc.enterprise_sonic.sonic_copp: + config: + copp_groups: + - copp_name: 'copp-4' + trap_priority: 4 + trap_action: 'FORWARD' + queue: 4 + cir: 200 + cbs: 200 + state: overridden + +# After state: +# ------------ +# +# sonic# show copp actions +# CoPP action group copp-4 +# trap-action forward +# trap-priority 4 +# trap-queue 4 +# police cir 200 cbs 200 +# +# +# Using deleted +# +# Before state: +# ------------- +# +# sonic# show copp actions +# CoPP action group copp-1 +# trap-action drop +# trap-priority 1 +# trap-queue 1 +# police cir 45 cbs 45 +# CoPP action group copp-2 +# trap-action forward +# trap-priority 2 +# trap-queue 2 +# police cir 90 cbs 90 + + - name: Delete CoPP groups configuration + dellemc.enterprise_sonic.sonic_copp: + config: + copp_groups: + - copp_name: 'copp-1' + trap_action: 'DROP' + cir: '45' + cbs: '45' + - copp_name: 'copp-2' + state: deleted + +# After state: +# ------------ +# +# sonic# show copp actions +# CoPP action group copp-1 +# trap-action drop +# police cir 45 cbs 45 + + +""" +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.copp.copp import CoppArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.copp.copp import Copp + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=CoppArgs.argument_spec, + supports_check_mode=True) + + result = Copp(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_dhcp_relay.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_dhcp_relay.py new file mode 100644 index 000000000..321a673eb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_dhcp_relay.py @@ -0,0 +1,781 @@ +#!/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_dhcp_relay +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_dhcp_relay +version_added: '2.1.0' +short_description: Manage DHCP and DHCPv6 relay configurations on SONiC +description: + - This module provides configuration management of DHCP and DHCPv6 relay + parameters on Layer 3 interfaces of devices running SONiC. + - Layer 3 interface and VRF name need to be created earlier in the device. +author: 'Arun Saravanan Balachandran (@ArunSaravananBalachandran)' +options: + config: + description: + - Specifies the DHCP and DHCPv6 relay configurations. + type: list + elements: dict + suboptions: + name: + description: + - Full name of the Layer 3 interface, i.e. Eth1/1. + type: str + required: true + ipv4: + description: + - DHCP relay configurations to be set for the interface mentioned in name option. + type: dict + suboptions: + server_addresses: + description: + - List of DHCP server IPv4 addresses. + type: list + elements: dict + suboptions: + address: + description: + - IPv4 address of the DHCP server. + type: str + vrf_name: + description: + - Specifies name of the VRF in which the DHCP server resides. + - This option is not used with state I(deleted). + type: str + source_interface: + description: + - Specifies the DHCP relay source interface. + type: str + max_hop_count: + description: + - Specifies the maximum hop count for DHCP relay packets. + - The range is from 1 to 16. + type: int + link_select: + description: + - Enable link selection suboption. + type: bool + vrf_select: + description: + - Enable VRF selection suboption. + type: bool + circuit_id: + description: + - Specifies the DHCP relay circuit-id format. + - C(%h:%p) - Hostname followed by interface name eg. sonic:Vlan100 + - C(%i) - Name of the physical interface eg. Eth1/2 + - C(%p) - Name of the interface eg. Vlan100 + type: str + choices: + - '%h:%p' + - '%i' + - '%p' + policy_action: + description: + - Specifies the policy for handling of DHCP relay options. + type: str + choices: + - append + - discard + - replace + ipv6: + description: + - DHCPv6 relay configurations to be set for the interface mentioned in name option. + type: dict + suboptions: + server_addresses: + description: + - List of DHCPv6 server IPv6 addresses. + type: list + elements: dict + suboptions: + address: + description: + - IPv6 address of the DHCPv6 server. + type: str + vrf_name: + description: + - Specifies name of the VRF in which the DHCPv6 server resides. + - This option is used only with state I(merged). + type: str + source_interface: + description: + - Specifies the DHCPv6 relay source interface. + type: str + max_hop_count: + description: + - Specifies the maximum hop count for DHCPv6 relay packets. + - The range is from 1 to 16. + type: int + vrf_select: + description: + - Enable VRF selection suboption. + type: bool + state: + description: + - The state of the configuration after module completion. + - C(merged) - Merges provided DHCP and DHCPv6 relay configuration with on-device configuration. + - C(deleted) - Deletes on-device DHCP and DHCPv6 relay configuration. + - C(replaced) - Replaces on-device DHCP and DHCPv6 relay configuration of the specified interfaces with provided configuration. + - C(overridden) - Overrides all on-device DHCP and DHCPv6 relay configurations with the provided configuration. + type: str + choices: + - merged + - deleted + - replaced + - overridden + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 92.1.1.1 vrf VrfReg1 +# ip dhcp-relay max-hop-count 5 +# ip dhcp-relay vrf-select +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ! + + - name: Delete DHCP and DHCPv6 relay configurations + dellemc.enterprise_sonic.sonic_dhcp_relay: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: '92.1.1.1' + vrf_select: true + max_hop_count: 5 + ipv6: + server_addresses: + - address: '91::1' + - address: '92::1' + - name: 'Eth1/2' + ipv4: + server_addresses: + - address: '71.1.1.1' + - address: '72.1.1.1' + source_interface: 'Vlan100' + link_select: true + circuit_id: '%h:%p' + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 vrf VrfReg1 +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 73.1.1.1 +# ! + + +# Using deleted +# +# NOTE: Support is provided in the dhcp_relay resource module for deletion of all attributes for a +# given address family (IPv4 or IPv6) by using a "special" YAML sequence specifying a server address list +# containing a single "blank" IP address under the target address family. The following example shows +# a task using this syntax for deletion of all DHCP (IPv4) configurations for an interface, but the +# equivalent syntax is supported for DHCPv6 (IPv6) as well. +# +# Before State: +# ------------- +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 92.1.1.1 vrf VrfReg1 +# ip dhcp-relay max-hop-count 5 +# ip dhcp-relay vrf-select +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ! + + - name: Delete all IPv4 DHCP relay configurations for interface Eth1/1 + dellemc.enterprise_sonic.sonic_dhcp_relay: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ! + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 92.1.1.1 vrf VrfReg1 +# ip dhcp-relay max-hop-count 5 +# ip dhcp-relay vrf-select +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ! + + - name: Delete all DHCP and DHCPv6 relay configurations for interface Eth1/1 + dellemc.enterprise_sonic.sonic_dhcp_relay: + config: + - name: 'Eth1/1' + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ipv6 address 81::1/24 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ! + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 92.1.1.1 vrf VrfReg1 +# ip dhcp-relay max-hop-count 5 +# ip dhcp-relay vrf-select +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ! + + - name: Delete all DHCP and DHCPv6 relay configurations + dellemc.enterprise_sonic.sonic_dhcp_relay: + config: + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ipv6 address 81::1/24 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ! + + +# Using merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ipv6 address 81::1/24 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 +# ! + + - name: Add DHCP and DHCPv6 relay configurations + dellemc.enterprise_sonic.sonic_dhcp_relay: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: '91.1.1.1' + - address: '92.1.1.1' + vrf_name: 'VrfReg1' + vrf_select: true + max_hop_count: 5 + policy_action: 'append' + ipv6: + server_addresses: + - address: '91::1' + - address: '92::1' + max_hop_count: 5 + - name: 'Eth1/2' + ipv4: + server_addresses: + - address: '73.1.1.1' + source_interface: 'Vlan100' + link_select: true + circuit_id: '%h:%p' + state: merged + +# After State: +# ------------ +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 92.1.1.1 vrf VrfReg1 +# ip dhcp-relay max-hop-count 5 +# ip dhcp-relay vrf-select +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ! + + +# Using replaced +# +# Before State: +# ------------- +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 92.1.1.1 vrf VrfReg1 +# ip dhcp-relay max-hop-count 5 +# ip dhcp-relay vrf-select +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ipv6 address 61::1/24 +# ipv6 dhcp-relay 71::1 72::1 +# ! +# interface Eth1/3 +# mtu 9100 +# speed 400000 +# fec RS +# shutdown +# ip address 41.1.1.1/24 +# ip dhcp-relay 51.1.1.1 52.1.1.1 +# ip dhcp-relay circuit-id %h:%p +# ipv6 address 41::1/24 +# ipv6 dhcp-relay 51::1 52::1 +# ! + + - name: Replace DHCP and DHCPv6 relay configurations of specified interfaces + dellemc.enterprise_sonic.sonic_dhcp_relay: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: '91.1.1.1' + - address: '93.1.1.1' + - address: '95.1.1.1' + vrf_name: 'VrfReg1' + vrf_select: true + ipv6: + server_addresses: + - address: '93::1' + - address: '94::1' + source_interface: 'Vlan100' + - name: 'Eth1/2' + ipv4: + server_addresses: + - address: '73.1.1.1' + circuit_id: '%h:%p' + state: replaced + +# After State: +# ------------ +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 93.1.1.1 95.1.1.1 vrf VrfReg1 +# ip dhcp-relay vrf-select +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 93::1 94::1 +# ipv6 dhcp-relay source-interface Vlan100 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 73.1.1.1 +# ip dhcp-relay circuit-id %h:%p +# ipv6 address 61::1/24 +# ! +# interface Eth1/3 +# mtu 9100 +# speed 400000 +# fec RS +# shutdown +# ip address 41.1.1.1/24 +# ip dhcp-relay 51.1.1.1 52.1.1.1 +# ip dhcp-relay circuit-id %h:%p +# ipv6 address 41::1/24 +# ipv6 dhcp-relay 51::1 52::1 +# ! + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 92.1.1.1 vrf VrfReg1 +# ip dhcp-relay max-hop-count 5 +# ip dhcp-relay vrf-select +# ip dhcp-relay policy-action append +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 91::1 92::1 +# ipv6 dhcp-relay max-hop-count 5 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 71.1.1.1 72.1.1.1 73.1.1.1 +# ip dhcp-relay source-interface Vlan100 +# ip dhcp-relay link-select +# ip dhcp-relay circuit-id %h:%p +# ipv6 address 61::1/24 +# ipv6 dhcp-relay 71::1 72::1 +# ! +# interface Eth1/3 +# mtu 9100 +# speed 400000 +# fec RS +# shutdown +# ip address 41.1.1.1/24 +# ip dhcp-relay 51.1.1.1 52.1.1.1 +# ip dhcp-relay circuit-id %h:%p +# ipv6 address 41::1/24 +# ipv6 dhcp-relay 51::1 52::1 +# ! + + - name: Override DHCP and DHCPv6 relay configurations + dellemc.enterprise_sonic.sonic_dhcp_relay: + config: + - name: 'Eth1/1' + ipv4: + server_addresses: + - address: '91.1.1.1' + - address: '93.1.1.1' + - address: '95.1.1.1' + vrf_name: 'VrfReg1' + vrf_select: true + ipv6: + server_addresses: + - address: '93::1' + - address: '94::1' + source_interface: 'Vlan100' + - name: 'Eth1/2' + ipv4: + server_addresses: + - address: '73.1.1.1' + circuit_id: '%h:%p' + state: overridden + +# After State: +# ------------ +# +# sonic# show running-configuration interface +# ! +# interface Eth1/1 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 81.1.1.1/24 +# ip dhcp-relay 91.1.1.1 93.1.1.1 95.1.1.1 vrf VrfReg1 +# ip dhcp-relay vrf-select +# ipv6 address 81::1/24 +# ipv6 dhcp-relay 93::1 94::1 +# ipv6 dhcp-relay source-interface Vlan100 +# ! +# interface Eth1/2 +# mtu 9100 +# speed 400000 +# fec RS +# no shutdown +# ip address 61.1.1.1/24 +# ip dhcp-relay 73.1.1.1 +# ip dhcp-relay circuit-id %h:%p +# ipv6 address 61::1/24 +# ! +# interface Eth1/3 +# mtu 9100 +# speed 400000 +# fec RS +# shutdown +# ip address 41.1.1.1/24 +# ipv6 address 41::1/24 +# ! + + +""" +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.dhcp_relay.dhcp_relay import Dhcp_relayArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.dhcp_relay.dhcp_relay import Dhcp_relay + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Dhcp_relayArgs.argument_spec, + supports_check_mode=True) + + result = Dhcp_relay(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_dhcp_snooping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_dhcp_snooping.py new file mode 100644 index 000000000..948ecb891 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_dhcp_snooping.py @@ -0,0 +1,499 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 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_dhcp_snooping +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_dhcp_snooping +version_added: 2.3.0 +notes: + - "Tested against Enterprise SONiC Distribution by Dell Technologies." +short_description: "Manage DHCP Snooping on SONiC" +description: "This module provides configuration management of DHCP snooping for devices running SONiC." +author: Simon Nathans (@simon-nathans), Xiao Han (@Xiao_Han2) +options: + config: + description: The DHCP snooping configuration. + type: dict + suboptions: + afis: + description: + - List of address families to configure. + - "There can be up to two items in this list: one where I(afi=ipv4) and one where I(afi=ipv6) to configure DHCPv4 and DHCPv6, respectively." + type: list + elements: dict + suboptions: + afi: + description: + - The address family to configure. + type: str + choices: ['ipv4', 'ipv6'] + required: true + enabled: + description: + - Enable DHCP snooping for I(afi). + type: bool + vlans: + description: + - Enable DHCP snooping on a list of VLANs for I(afi). + - When I(state=deleted), passing an empty list will disable DHCP snooping in all VLANs + type: list + elements: str + verify_mac: + description: + - Enable DHCP snooping MAC verification for I(afi). + type: bool + trusted: + description: + - Mark interfaces as trusted for DHCP snooping for I(afi). + - When I(state=deleted), passing an empty list will delete all trusted interfaces. + type: list + elements: dict + suboptions: + intf_name: + description: + - The interface name. + type: str + required: true + source_bindings: + description: + - Create a static entry in the DHCP snooping binding database for I(afi). + - When I(state=deleted), passing an empty list will delete all source bindings. + type: list + elements: dict + suboptions: + mac_addr: + description: + - The binding's MAC address. + type: str + required: true + ip_addr: + description: + - The bindings's IP address. + type: str + intf_name: + description: + - The binding's interface name. + - Can be an Ethernet or a PortChannel interface. + type: str + vlan_id: + description: + - The binding's VLAN ID. + type: int + state: + description: + - The state of the configuration after module completion. + default: merged + choices: ['merged', 'deleted', 'overridden', 'replaced'] + type: str +""" +EXAMPLES = """ +# Using merged +# +# Before State: +# ------------- +# +# sonic# show ip dhcp snooping +# ! +# DHCP snooping is Disabled +# DHCP snooping source MAC verification is Disabled +# DHCP snooping is enabled on the following VLANs: +# DHCP snooping trusted interfaces: +# ! + +- name: Configure DHCPv4 snooping global settings + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv4' + enabled: true + verify_mac: true + vlans: ['1', '2', '3', '5'] + trusted: + - intf_name: 'Ethernet8' + state: merged + +# After State: +# ------------ +# +# sonic# show ip dhcp snooping +# ! +# DHCP snooping is Enabled +# DHCP snooping source MAC verification is Enabled +# DHCP snooping is enabled on the following VLANs: 1 2 3 5 +# DHCP snooping trusted interfaces: Ethernet8 +# ! + + +# Using merged +# +# Before State: +# ------------- +# +# sonic# show ipv6 dhcp snooping +# ! +# DHCPv6 snooping is Disabled +# DHCPv6 snooping source MAC verification is Disabled +# DHCPv6 snooping is enabled on the following VLANs: +# DHCPv6 snooping trusted interfaces: +# ! + +- name: Configure DHCPv6 snooping global settings + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv6' + enabled: true + vlans: + - '4' + trusted: + - intf_name: 'Ethernet2' + - intf_name: PortChannel1 + state: merged + +# After State: +# ------------ +# +# sonic# show ipv6 dhcp snooping +# ! +# DHCPv6 snooping is Enabled +# DHCPv6 snooping source MAC verification is Disabled +# DHCPv6 snooping is enabled on the following VLANs: 4 +# DHCPv6 snooping trusted interfaces: PortChannel1 +# ! + + +# Using merged +# +# Before State: +# ------------- +# +# sonic# show ip dhcp snooping binding +# ! +# Total number of Dynamic bindings: 0 +# Total number of Static bindings: 0 +# Total number of Tentative bindings: 0 +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# ! + +- name: Add DHCPv4 snooping bindings + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv4' + source_bindings: + - mac_addr: '00:b0:d0:63:c2:26' + ip_addr: '192.0.2.146' + intf_name: 'Ethernet4' + vlan_id: '1' + - mac_addr: 'aa:f7:67:fc:f4:9a' + ip_addr: '156.33.90.167' + intf_name: 'PortChannel1' + vlan_id: '2' + state: merged + +# After State: +# ------------ +# +# sonic# show ip dhcp snooping binding +# ! +# Total number of Dynamic bindings: 0 +# Total number of Static bindings: 2 +# Total number of Tentative bindings: 0 +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# 00:b0:d0:63:c2:26 192.0.2.146 1 Ethernet4 static NA +# aa:f7:67:fc:f4:9a 156.33.90.167 2 PortChannel1 static NA +# ! + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show ip dhcp snooping +# ! +# DHCP snooping is Enabled +# DHCP snooping source MAC verification is Enabled +# DHCP snooping is enabled on the following VLANs: 1 2 3 5 +# DHCP snooping trusted interfaces: Ethernet8 +# ! + +- name: Disable DHCPv4 snooping on some VLANs + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv4' + vlans: + - '3' + - '5' + state: deleted + +# After State: +# ------------ +# +# sonic# show ip dhcp snooping +# ! +# DHCP snooping is Enabled +# DHCP snooping source MAC verification is Enabled +# DHCP snooping is enabled on the following VLANs: 1 2 +# DHCP snooping trusted interfaces: +# ! + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show ipv6 dhcp snooping +# ! +# DHCPv6 snooping is Enabled +# DHCPv6 snooping source MAC verification is Disabled +# DHCPv6 snooping is enabled on the following VLANs: 4 +# DHCPv6 snooping trusted interfaces: PortChannel1 PortChannel2 PortChannel3 PortChannel4 +# ! + +- name: Disable DHCPv6 snooping on all VLANs + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv6' + vlans: [] + state: deleted + +# After State: +# ------------ +# +# sonic# show ipv6 dhcp snooping +# ! +# DHCPv6 snooping is Enabled +# DHCPv6 snooping source MAC verification is Disabled +# DHCPv6 snooping is enabled on the following VLANs: +# DHCPv6 snooping trusted interfaces: PortChannel1 PortChannel2 PortChannel3 PortChannel4 +# ! + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show ipv6 dhcp snooping +# ! +# DHCPv6 snooping is Enabled +# DHCPv6 snooping source MAC verification is Disabled +# DHCPv6 snooping is enabled on the following VLANs: 4 +# DHCPv6 snooping trusted interfaces: PortChannel1 PortChannel2 PortChannel3 PortChannel4 +# ! + +- name: Delete all DHCPv6 configuration + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv6' + state: deleted + +# After State: +# ------------ +# +# sonic# show ipv6 dhcp snooping +# ! +# DHCPv6 snooping is Disabled +# DHCPv6 snooping source MAC verification is Disabled +# DHCPv6 snooping is enabled on the following VLANs: +# DHCPv6 snooping trusted interfaces: +# ! + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show ip dhcp snooping binding +# ! +# Total number of Dynamic bindings: 0 +# Total number of Static bindings: 2 +# Total number of Tentative bindings: 0 +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# 00:b0:d0:63:c2:26 192.0.2.146 1 Ethernet4 static NA +# aa:f7:67:fc:f4:9a 156.33.90.167 2 PortChannel1 static NA +# ! + +- name: Delete a DHCPv4 snooping binding + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv4' + source_bindings: + - mac_addr: '00:b0:d0:63:c2:26' + ip_addr: '192.0.2.146' + intf_name: 'Ethernet4' + vlan_id: '1' + state: deleted + +# After State: +# ------------ +# +# sonic# show ip dhcp snooping binding +# ! +# Total number of Dynamic bindings: 0 +# Total number of Static bindings: 2 +# Total number of Tentative bindings: 0 +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# aa:f7:67:fc:f4:9a 156.33.90.167 2 PortChannel1 static NA +# ! + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show ipv4 dhcp snooping binding +# ! +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# 00:b0:d0:63:c2:26 192.0.2.146 1 Ethernet4 static NA +# 28:21:28:15:c1:1b 141.202.222.118 1 Ethernet2 static NA +# aa:f7:67:fc:f4:9a 156.33.90.167 2 PortChannel1 static NA +# ! + +- name: Override DHCPv4 snooping bindings + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv4' + source_bindings: + - mac_addr: '00:b0:d0:63:c2:26' + ip_addr: '192.0.2.146' + intf_name: 'Ethernet4' + vlan_id: '3' + state: overridden + +# After State: +# ------------ +# +# sonic# show ipv4 dhcp snooping binding +# ! +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# 00:b0:d0:63:c2:26 192.0.2.146 3 Ethernet4 static NA +# ! + + +# Using replaced +# +# Before State: +# ------------- +# +# sonic# show ipv4 dhcp snooping binding +# ! +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# 00:b0:d0:63:c2:26 192.0.2.146 1 Ethernet4 static NA +# 28:21:28:15:c1:1b 141.202.222.118 1 Ethernet2 static NA +# aa:f7:67:fc:f4:9a 156.33.90.167 2 PortChannel1 static NA +# ! + +- name: Replace DHCPv4 snooping bindings + dellemc.enterprise_sonic.sonic_dhcp_snooping: + config: + afis: + - afi: 'ipv4' + source_bindings: + - mac_addr: '00:b0:d0:63:c2:26' + ip_addr: '192.0.2.146' + intf_name: 'Ethernet4' + vlan_id: '3' + state: replaced + +# After State: +# ------------ +# +# sonic# show ipv4 dhcp snooping binding +# ! +# MAC Address IP Address VLAN Interface Type Lease (Secs) +# ----------------- --------------- ---- ----------- ------- ----------- +# 00:b0:d0:63:c2:26 192.0.2.146 3 Ethernet4 static NA +# 28:21:28:15:c1:1b 141.202.222.118 1 Ethernet2 static NA +# aa:f7:67:fc:f4:9a 156.33.90.167 2 PortChannel1 static NA +# ! + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + 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.dhcp_snooping.dhcp_snooping import Dhcp_snoopingArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.dhcp_snooping.dhcp_snooping import Dhcp_snooping + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Dhcp_snoopingArgs.argument_spec, + supports_check_mode=True) + + result = Dhcp_snooping(module).execute_module() + 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 index f13e9defd..3fa261381 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_facts.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_facts.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) """ @@ -67,6 +67,7 @@ options: - bgp_ext_communities - mclag - prefix_lists + - vlan_mapping - vrfs - vxlans - users @@ -77,6 +78,21 @@ options: - radius_server - static_routes - ntp + - logging + - pki + - ip_neighbor + - port_group + - dhcp_relay + - acl_interfaces + - l2_acls + - l3_acls + - lldp_global + - mac + - bfd + - copp + - route_maps + - stp + - dhcp_snooping """ EXAMPLES = """ diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_interfaces.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_interfaces.py index 0cd6a1896..09a5d0e18 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_interfaces.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_interfaces.py @@ -63,12 +63,53 @@ options: description: - MTU of the interface. type: int + speed: + description: + - Interface speed. + - Supported speeds are dependent on the type of switch. + type: str + choices: + - SPEED_10MB + - SPEED_100MB + - SPEED_1GB + - SPEED_2500MB + - SPEED_5GB + - SPEED_10GB + - SPEED_20GB + - SPEED_25GB + - SPEED_40GB + - SPEED_50GB + - SPEED_100GB + - SPEED_400GB + auto_negotiate: + description: + - auto-negotiate transmission parameters with peer interface. + type: bool + advertised_speed: + description: + - Advertised speeds of the interface. + - Supported speeds are dependent on the type of switch. + - Speeds may be 10, 100, 1000, 2500, 5000, 10000, 20000, 25000, 40000, 50000, 100000 or 400000. + type: list + elements: str + fec: + description: + - Interface FEC (Forward Error Correction). + type: str + choices: + - FEC_RS + - FEC_FC + - FEC_DISABLED + - FEC_DEFAULT + - FEC_AUTO state: description: - The state the configuration should be left in. type: str choices: - merged + - replaced + - overridden - deleted default: merged """ @@ -80,18 +121,28 @@ EXAMPLES = """ # # show interface status | no-more #------------------------------------------------------------------------------------------ -#Name Description Admin Oper Speed MTU +#Name Description Admin Oper AutoNeg 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 +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 Ethernet-8 down 100000 9100 +#Ethernet12 Ethernet-12 down on - 5000 +#Ethernet16 - down 40000 9100 # -- name: Configures interfaces - dellemc.enterprise_sonic.sonic_interfaces: +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed 100000 +# fec AUTO +# shutdown +# +- name: Configure interfaces + sonic_interfaces: config: - name: Eth1/3 + - name: Ethernet8 + - name: Ethernet12 + - name: Ethernet16 state: deleted # # After state: @@ -99,14 +150,20 @@ EXAMPLES = """ # # show interface status | no-more #------------------------------------------------------------------------------------------ -#Name Description Admin Oper Speed MTU +#Name Description Admin Oper AutoNeg 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 +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - up 100000 9100 +#Ethernet12 - up 100000 9100 +#Ethernet16 - up 100000 9100 # +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed 100000 +# shutdown # # Using deleted # @@ -115,33 +172,33 @@ EXAMPLES = """ # # show interface status | no-more #------------------------------------------------------------------------------------------ -#Name Description Admin Oper Speed MTU +#Name Description Admin Oper AutoNeg 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 +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - down 100000 9100 +#Ethernet12 - down 1000 9100 +#Ethernet16 - down 100000 9100 # - -- name: Configures interfaces - dellemc.enterprise_sonic.sonic_interfaces: +- name: Configure interfaces + sonic_interfaces: config: - state: deleted + state: deleted # # After state: # ------------- # # show interface status | no-more #------------------------------------------------------------------------------------------ -#Name Description Admin Oper Speed MTU +#Name Description Admin Oper AutoNeg 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 +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - up 100000 9100 +#Ethernet12 - up 100000 9100 +#Ethernet16 - up 100000 9100 +# # # # Using merged @@ -151,38 +208,177 @@ EXAMPLES = """ # # show interface status | no-more #------------------------------------------------------------------------------------------ -#Name Description Admin Oper Speed MTU +#Name Description Admin Oper AutoNeg Speed MTU #------------------------------------------------------------------------------------------ -#Eth1/1 - up 100000 9100 -#Eth1/2 - up 100000 9100 -#Eth1/3 - down 100000 9100 -#Eth1/3 - down 1000 9100 +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - down 100000 9100 +#Ethernet12 - down 100000 9100 +#Ethernet16 - down 100000 9100 +# +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed 100000 +# shutdown # -- name: Configures interfaces - dellemc.enterprise_sonic.sonic_interfaces: +- name: Configure interfaces + sonic_interfaces: config: - - name: Eth1/3 - description: 'Ethernet Twelve' - - name: Eth1/5 - description: 'Ethernet Sixteen' - enable: True - mtu: 3500 + - name: Ethernet8 + fec: FEC_AUTO + - name: Ethernet12 + description: 'Ethernet Twelve' + auto_negotiate: True + - name: Ethernet16 + description: 'Ethernet Sixteen' + enabled: True + mtu: 3500 + speed: SPEED_40GB state: merged # +# After state: +# ------------ +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper AutoNeg Speed MTU +#------------------------------------------------------------------------------------------ +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - down 100000 9100 +#Ethernet12 Ethernet Twelve down on 100000 9100 +#Ethernet16 Ethernet Sixteen up 40000 3500 +# +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed 100000 +# fec AUTO +# shutdown +# +# Using overridden +# +# Before state: +# ------------- +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper AutoNeg Speed MTU +#------------------------------------------------------------------------------------------ +#Ethernet0 E0 up 100000 9100 +#Ethernet4 E4 up 100000 9100 +#Ethernet8 E8 down 100000 9100 +#Ethernet12 - down 1000 9100 +#Ethernet16 - down 100000 9100 +# +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed 100000 +# shutdown +# +- name: Configure interfaces + sonic_interfaces: + config: + - name: Ethernet8 + fec: FEC_AUTO + - name: Ethernet12 + description: 'Ethernet Twelve' + mtu: 3500 + enabled: True + auto_negotiate: True + - name: Ethernet16 + description: 'Ethernet Sixteen' + mtu: 3000 + enabled: False + speed: SPEED_40GB + state: overridden +# +# After state: +# ------------ +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper AutoNeg Speed MTU +#------------------------------------------------------------------------------------------ +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - up 100000 9100 +#Ethernet12 Ethernet Twelve up on 100000 3500 +#Ethernet16 Ethernet Sixteen down 40000 3000 +# +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed 100000 +# fec AUTO +# no shutdown +# +# Using replaced +# +# Before state: +# ------------- +# +# show interface status | no-more +#------------------------------------------------------------------------------------------ +#Name Description Admin Oper AutoNeg Speed MTU +#------------------------------------------------------------------------------------------ +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - down on 100000 9100 +#Ethernet12 - down 1000 9100 +#Ethernet16 - down 100000 9100 +# +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed auto 40000 +# shutdown +# +- name: Configure interfaces + sonic_interfaces: + config: + - name: Ethernet8 + advertised_speed: + - "100000" + - name: Ethernet12 + description: 'Ethernet Twelve' + mtu: 3500 + enabled: True + auto_negotiate: True + - name: Ethernet16 + description: 'Ethernet Sixteen' + mtu: 3000 + enabled: False + speed: SPEED_40GB + state: replaced # # After state: # ------------ # # show interface status | no-more #------------------------------------------------------------------------------------------ -#Name Description Admin Oper Speed MTU +#Name Description Admin Oper AutoNeg 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 +#Ethernet0 - up 100000 9100 +#Ethernet4 - up 100000 9100 +#Ethernet8 - down on 100000 9100 +#Ethernet12 Ethernet Twelve up on 100000 3500 +#Ethernet16 Ethernet Sixteen down 40000 3000 # +# show running-configuration interface Ethernet 8 +#! +#interface Ethernet8 +# mtu 9100 +# speed auto 100000 +# fec AUTO +# shutdown # """ RETURN = """ diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ip_neighbor.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ip_neighbor.py new file mode 100644 index 000000000..f1e4acc82 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ip_neighbor.py @@ -0,0 +1,300 @@ +#!/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_ip_neighbor +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_ip_neighbor +version_added: 2.1.0 +notes: + - Supports C(check_mode). +short_description: Manage IP neighbor global configuration on SONiC. +description: + - This module provides configuration management of IP neighbor global for devices running SONiC. +author: "M. Zhang (@mingjunzhang2019)" +options: + config: + description: + - Specifies IP neighbor global configurations. + type: dict + suboptions: + ipv4_arp_timeout: + type: int + description: + - IPv4 ARP timeout. + - The range is from 60 to 14400. + ipv6_nd_cache_expiry: + type: int + description: + - IPv6 ND cache expiry. + - The range is from 60 to 14400. + num_local_neigh: + type: int + description: + - The number of reserved local neighbors. + - The range is from 0 to 32000. + ipv4_drop_neighbor_aging_time: + type: int + description: + - IPv4 drop neighbor aging time. + - The range is from 60 to 14400. + ipv6_drop_neighbor_aging_time: + type: int + description: + - IPv6 drop neighbor aging time. + - The range is from 60 to 14400. + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# +# Using merged +# +# Before state: +# ------------- +# +#sonic# show running-configuration +#! +#ip arp timeout 180 +#ip drop-neighbor aging-time 300 +#ipv6 drop-neighbor aging-time 300 +#ip reserve local-neigh 0 +#ipv6 nd cache expire 180 +#! +- name: Configure IP neighbor global + sonic_ip_neighbor: + config: + ipv4_arp_timeout: 1200 + ipv4_drop_neighbor_aging_time: 600 + ipv6_drop_neighbor_aging_time: 600 + ipv6_nd_cache_expiry: 1200 + num_local_neigh: 1000 + state: merged + +# After state: +# ------------ +# +#sonic# show running-configuration +#! +#ip arp timeout 1200 +#ip drop-neighbor aging-time 600 +#ipv6 drop-neighbor aging-time 600 +#ip reserve local-neigh 1000 +#ipv6 nd cache expire 1200 +#! +# +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show running-configuration +#! +#ip arp timeout 1200 +#ip drop-neighbor aging-time 600 +#ipv6 drop-neighbor aging-time 600 +#ip reserve local-neigh 1000 +#ipv6 nd cache expire 1200 +#! +- name: Delete some IP neighbor configuration + sonic_ip_neighbor: + config: + ipv4_arp_timeout: 0 + ipv4_drop_neighbor_aging_time: 0 + state: deleted + +# After state: +# ------------ +# +#sonic# show running-configuration +#! +#ip arp timeout 180 +#ip drop-neighbor aging-time 300 +#ipv6 drop-neighbor aging-time 600 +#ip reserve local-neigh 1000 +#ipv6 nd cache expire 1200 +#! +# +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show running-configuration +#! +#ip arp timeout 1200 +#ip drop-neighbor aging-time 600 +#ipv6 drop-neighbor aging-time 600 +#ip reserve local-neigh 1000 +#ipv6 nd cache expire 1200 +#! +- name: Delete all IP neighbor configuration + sonic_ip_neighbor: + config: {} + state: deleted + +# After state: +# ------------ +# +#sonic# show running-configuration +#! +#ip arp timeout 180 +#ip drop-neighbor aging-time 300 +#ipv6 drop-neighbor aging-time 300 +#ip reserve local-neigh 0 +#ipv6 nd cache expire 180 +#! +# +# Using replaced +# +# Before state: +# ------------- +# +#sonic# show running-configuration +#! +#ip arp timeout 1200 +#ip drop-neighbor aging-time 600 +#ipv6 drop-neighbor aging-time 300 +#ip reserve local-neigh 0 +#ipv6 nd cache expire 180 +#! +- name: Change some IP neighbor configuration + sonic_ip_neighbor: + config: + ipv6_drop_neighbor_aging_time: 600 + ipv6_nd_cache_expiry: 1200 + num_local_neigh: 1000 + state: replaced + +# After state: +# ------------ +# +#sonic# show running-configuration +#! +#ip arp timeout 1200 +#ip drop-neighbor aging-time 600 +#ipv6 drop-neighbor aging-time 600 +#ip reserve local-neigh 1000 +#ipv6 nd cache expire 1200 +#! +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic# show running-configuration +#! +#ip arp timeout 1200 +#ip drop-neighbor aging-time 600 +#ipv6 drop-neighbor aging-time 300 +#ip reserve local-neigh 0 +#ipv6 nd cache expire 180 +#! +- name: Reset IP neighbor configuration, then configure some + sonic_ip_neighbor: + config: + ipv6_drop_neighbor_aging_time: 600 + ipv6_nd_cache_expiry: 1200 + num_local_neigh: 1000 + state: overridden + +# After state: +# ------------ +# +#sonic# show running-configuration +#! +#ip arp timeout 180 +#ip drop-neighbor aging-time 300 +#ipv6 drop-neighbor aging-time 600 +#ip reserve local-neigh 1000 +#ipv6 nd cache expire 1200 +#! +# +""" +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. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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.ip_neighbor.ip_neighbor import Ip_neighborArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.ip_neighbor.ip_neighbor import Ip_neighbor + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Ip_neighborArgs.argument_spec, + supports_check_mode=True) + + result = Ip_neighbor(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_acls.py new file mode 100644 index 000000000..cda50242b --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_acls.py @@ -0,0 +1,582 @@ +#!/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_l2_acls +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_l2_acls +version_added: '2.1.0' +notes: + - Supports C(check_mode). +short_description: Manage Layer 2 access control lists (ACL) configurations on SONiC +description: + - This module provides configuration management of Layer 2 access control lists (ACL) + in devices running SONiC. +author: 'Arun Saravanan Balachandran (@ArunSaravananBalachandran)' +options: + config: + description: + - Specifies Layer 2 ACL configurations. + type: list + elements: dict + suboptions: + name: + description: + - Specifies the ACL name. + type: str + required: true + remark: + description: + - Specifies remark for the ACL. + type: str + rules: + description: + - List of rules with the ACL. + - I(sequence_num), I(action), I(source) & I(destination) are required for adding a new rule. + - If I(state=deleted), options other than I(sequence_num) are not considered. + - I(ethertype) and I(vlan_tag_format) are mutually exclusive. + type: list + elements: dict + suboptions: + sequence_num: + description: + - Specifies the sequence number of the rule. + - The range is from 1 to 65535. + type: int + required: true + action: + description: + - Specifies the action taken on the matched Ethernet frame. + type: str + choices: + - deny + - discard + - do-not-nat + - permit + - transit + source: + description: + - Specifies the source of the Ethernet frame. + - I(address) and I(address_mask) are required together. + - I(any), I(host) and I(address) are mutually exclusive. + type: dict + suboptions: + any: + description: + - Match any source MAC address. + type: bool + host: + description: + - MAC address of a single source host. + type: str + address: + description: + - Source MAC address. + type: str + address_mask: + description: + - Source MAC address mask. + type: str + destination: + description: + - Specifies the destination of the Ethernet frame. + - I(address) and I(address_mask) are required together. + - I(any), I(host) and I(address) are mutually exclusive. + type: dict + suboptions: + any: + description: + - Match any destination MAC address. + type: bool + host: + description: + - MAC address of a single destination host. + type: str + address: + description: + - Destination MAC address. + type: str + address_mask: + description: + - Destination MAC address mask. + type: str + ethertype: + description: + - Specifies the EtherType of the Ethernet frame. + - Only one suboption can be specified for ethertype in a rule. + type: dict + suboptions: + value: + description: + - Specifies the EtherType value to match as a hexadecimal string. + - The range is from 0x600 to 0xffff. + type: str + arp: + description: + - Match Ethernet frame with ARP EtherType (0x806). + type: bool + ipv4: + description: + - Match Ethernet frame with IPv4 EtherType (0x800). + type: bool + ipv6: + description: + - Match Ethernet frame with IPv6 EtherType (0x86DD). + type: bool + vlan_id: + description: + - Match Ethernet frame with the given VLAN ID. + type: int + vlan_tag_format: + description: + - Match Ethernet frame with the given VLAN tag format. + type: dict + suboptions: + multi_tagged: + description: + - Match three of more VLAN tagged Ethernet frame. + type: bool + dei: + description: + - Match Ethernet frame with the given Drop Eligible Indicator (DEI) value. + type: int + choices: + - 0 + - 1 + pcp: + description: + - Match Ethernet frames using Priority Code Point (PCP) value. + - I(mask) is valid only when I(value) is specified. + - I(value) and I(traffic_type) are mutually exclusive. + type: dict + suboptions: + value: + description: + - Match Ethernet frame with the given PCP value. + - The range is from 0 to 7 + type: int + mask: + description: + - Match Ethernet frame with given PCP value and mask. + - The range is from 0 to 7. + type: int + traffic_type: + description: + - Match Ethernet frame with PCP value for the given traffic type. + - C(be) - Match Ethernet frame with Best effort PCP (0). + - C(bk) - Match Ethernet frame with Background PCP (1). + - C(ee) - Match Ethernet frame with Excellent effort PCP (2). + - C(ca) - Match Ethernet frame with Critical applications PCP (3). + - C(vi) - Match Ethernet frame with Video, < 100 ms latency and jitter PCP (4). + - C(vo) - Match Ethernet frame with Voice, < 10 ms latency and jitter PCP (5). + - C(ic) - Match Ethernet frame with Internetwork control PCP (6). + - C(nc) - Match Ethernet frame with Network control PCP (7). + type: str + choices: + - be + - bk + - ee + - ca + - vi + - vo + - ic + - nc + remark: + description: + - Specifies remark for the ACL rule. + type: str + state: + description: + - The state of the configuration after module completion. + - C(merged) - Merges provided L2 ACL configuration with on-device configuration. + - C(replaced) - Replaces on-device configuration of the specified L2 ACLs with provided configuration. + - C(overridden) - Overrides all on-device L2 ACL configurations with the provided configuration. + - C(deleted) - Deletes on-device L2 ACL configuration. + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# Using merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# sonic# + + - name: Merge provided Layer 2 ACL configurations + dellemc.enterprise_sonic.sonic_l2_acls: + config: + - name: 'test' + rules: + - sequence_num: 2 + action: 'permit' + source: + any: true + destination: + any: true + ethertype: + value: '0x88cc' + remark: 'LLDP' + - sequence_num: 3 + action: 'permit' + source: + any: true + destination: + address: '00:00:10:00:00:00' + address_mask: '00:00:ff:ff:00:00' + pcp: + value: 4 + mask: 6 + - sequence_num: 4 + action: 'deny' + source: + any: true + destination: + any: true + vlan_tag_format: + multi_tagged: true + - name: 'test1' + remark: 'test_mac_acl' + rules: + - sequence_num: 1 + action: 'permit' + source: + host: '11:11:11:11:11:11' + destination: + any: true + - sequence_num: 2 + action: 'permit' + source: + any: true + destination: + any: true + ethertype: + arp: true + vlan_id: 100 + - sequence_num: 3 + action: 'deny' + source: + any: true + destination: + any: true + dei: 0 + state: merged + +# After State: +# ------------ +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# seq 2 permit any any 0x88cc remark LLDP +# seq 3 permit any 00:00:10:00:00:00 00:00:ff:ff:00:00 pcp vi pcp-mask 6 +# seq 4 deny any any vlan-tag-format multi-tagged +# ! +# mac access-list test1 +# remark test_mac_acl +# seq 1 permit host 11:11:11:11:11:11 any +# seq 2 permit any any arp vlan 100 +# seq 3 deny any any dei 0 +# sonic# + + +# Using replaced +# +# Before State: +# ------------- +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# seq 2 permit any any 0x88cc remark LLDP +# seq 3 permit any 00:00:10:00:00:00 00:00:ff:ff:00:00 pcp vi pcp-mask 6 +# ! +# mac access-list test1 +# remark test_mac_acl +# seq 1 permit host 11:11:11:11:11:11 any +# seq 2 permit any any arp vlan 100 +# seq 3 deny any any dei 0 +# sonic# + + - name: Replace device configuration of specified Layer 2 ACLs with provided configuration + dellemc.enterprise_sonic.sonic_l2_acls: + config: + - name: 'test1' + rules: + - sequence_num: 1 + action: 'permit' + source: + any: true + destination: + any: true + ethertype: + arp: true + vlan_id: 200 + - sequence_num: 2 + action: 'discard' + source: + any: true + destination: + any: true + - name: 'test2' + rules: + - sequence_num: 1 + action: 'permit' + source: + host: '33:33:33:33:33:33' + destination: + host: '44:44:44:44:44:44' + state: replaced + +# After State: +# ------------ +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# seq 2 permit any any 0x88cc remark LLDP +# seq 3 permit any 00:00:10:00:00:00 00:00:ff:ff:00:00 pcp vi pcp-mask 6 +# ! +# mac access-list test1 +# seq 1 permit any any arp vlan 200 +# seq 2 discard any any +# ! +# mac access-list test2 +# seq 1 permit host 33:33:33:33:33:33 host 44:44:44:44:44:44 +# sonic# + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# seq 2 permit any any 0x88cc remark LLDP +# seq 3 permit any 00:00:10:00:00:00 00:00:ff:ff:00:00 pcp vi pcp-mask 6 +# ! +# mac access-list test1 +# seq 1 permit any any arp vlan 200 +# seq 2 discard any any +# ! +# mac access-list test2 +# seq 1 permit host 33:33:33:33:33:33 host 44:44:44:44:44:44 +# sonic# + + - name: Override device configuration of all Layer 2 ACLs with provided configuration + dellemc.enterprise_sonic.sonic_l2_acls: + config: + - name: 'test1' + remark: 'test_mac_acl' + rules: + - sequence_num: 1 + action: 'permit' + source: + host: '11:11:11:11:11:11' + destination: + any: true + vlan_id: 100 + - sequence_num: 2 + action: 'permit' + source: + any: true + destination: + any: true + pcp: + traffic_type: 'ca' + - sequence_num: 3 + action: 'deny' + source: + any: true + destination: + any: true + ethertype: + ipv4: true + state: overridden + +# After State: +# ------------ +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test1 +# remark test_mac_acl +# seq 1 permit host 11:11:11:11:11:11 any vlan 100 +# seq 2 permit any any pcp ca +# seq 3 deny any any ip +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# seq 2 permit any any 0x88cc remark LLDP +# seq 3 permit any 00:00:10:00:00:00 00:00:ff:ff:00:00 pcp vi pcp-mask 6 +# ! +# mac access-list test1 +# remark test_mac_acl +# seq 1 permit host 11:11:11:11:11:11 any vlan 100 +# seq 2 deny any any ip +# ! +# mac access-list test2 +# seq 1 permit host 33:33:33:33:33:33 host 44:44:44:44:44:44 +# sonic# + + - name: Delete specified Layer 2 ACLs, ACL remark and ACL rule entries + dellemc.enterprise_sonic.sonic_l2_acls: + config: + - name: 'test' + rules: + - sequence_num: 3 + - name: 'test1' + remark: 'test_mac_acl' + - name: 'test2' + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# seq 2 permit any any 0x88cc remark LLDP +# ! +# mac access-list test1 +# seq 1 permit host 11:11:11:11:11:11 any vlan 100 +# seq 2 deny any any ip +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration mac access-list +# ! +# mac access-list test +# seq 1 permit host 22:22:22:22:22:22 any vlan 20 +# seq 2 permit any any 0x88cc remark LLDP +# seq 3 permit any 00:00:10:00:00:00 00:00:ff:ff:00:00 pcp vi pcp-mask 6 +# ! +# mac access-list test1 +# remark test_mac_acl +# seq 1 permit host 11:11:11:11:11:11 any vlan 100 +# seq 2 deny any any ip +# ! +# mac access-list test2 +# seq 1 permit host 33:33:33:33:33:33 host 44:44:44:44:44:44 +# sonic# + + - name: Delete all Layer 2 ACL configurations + dellemc.enterprise_sonic.sonic_l2_acls: + config: + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration mac access-list +# sonic# + + +""" +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. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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.l2_acls.l2_acls import L2_aclsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.l2_acls.l2_acls import L2_acls + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=L2_aclsArgs.argument_spec, + supports_check_mode=True) + + result = L2_acls(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 index 34a8ff720..8d70f8a3f 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_interfaces.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l2_interfaces.py @@ -54,13 +54,13 @@ options: description: Configures trunking parameters on an interface. suboptions: allowed_vlans: - description: Specifies list of allowed VLANs of trunk mode on the interface. + description: Specifies a list of allowed trunk mode VLANs and VLAN ranges for the interface. type: list elements: dict suboptions: vlan: - type: int - description: Configures the specified VLAN in trunk mode. + type: str + description: Configures the specified trunk mode VLAN or VLAN range. access: type: dict description: Configures access mode characteristics of the interface. @@ -74,6 +74,8 @@ options: choices: - merged - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -145,6 +147,47 @@ EXAMPLES = """ #15 Inactive # # +# Using deleted +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#11 Inactive T Ethernet12 +#12 Inactive A Ethernet12 +#13 Inactive T Ethernet12 +#14 Inactive T Ethernet12 +#15 Inactive T Ethernet12 +#16 Inactive T Ethernet12 + +- name: Delete the access vlan and a range of trunk vlans for an interface + sonic_l2_interfaces: + config: + - name: Ethernet12 + access: + vlan: 12 + trunk: + allowed_vlans: + - vlan: 13-16 + state: deleted + +# After state: +# ------------ +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#11 Inactive T Ethernet12 +#12 Inactive +#13 Inactive +#14 Inactive +#15 Inactive +#16 Inactive +# +# +# # Using merged # # Before state: @@ -153,10 +196,11 @@ EXAMPLES = """ #do show Vlan #Q: A - Access (Untagged), T - Tagged #NUM Status Q Ports +#10 Inactive #11 Inactive T Eth1/7 #12 Inactive T Eth1/7 # -- name: Configures switch port of interfaces +- name: Configures an access vlan for an interface dellemc.enterprise_sonic.sonic_l2_interfaces: config: - name: Eth1/3 @@ -184,15 +228,23 @@ EXAMPLES = """ #Q: A - Access (Untagged), T - Tagged #NUM Status Q Ports #10 Inactive A Eth1/3 +#12 Inactive +#13 Inactive +#14 Inactive +#15 Inactive +#16 Inactive +#18 Inactive # -- name: Configures switch port of interfaces +- name: Modify the access vlan, add a range of trunk vlans and a single trunk vlan for an interface dellemc.enterprise_sonic.sonic_l2_interfaces: config: - name: Eth1/3 + access: + vlan: 12 trunk: allowed_vlans: - - vlan: 11 - - vlan: 12 + - vlan: 13-16 + - vlan: 18 state: merged # # After state: @@ -201,9 +253,13 @@ EXAMPLES = """ #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 +#10 Inactive +#12 Inactive A Eth1/3 +#13 Inactive T Eth1/3 +#14 Inactive T Eth1/3 +#15 Inactive T Eth1/3 +#16 Inactive T Eth1/3 +#18 Inactive T Eth1/3 # # # Using merged @@ -250,6 +306,89 @@ EXAMPLES = """ #15 Inactive T Eth1/5 # # +# Using replaced +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Ethernet12 +# A Ethernet13 +#11 Inactive T Ethernet12 +# T Ethernet13 + +- name: Replace access vlan and trunk vlans for specified interfaces + sonic_l2_interfaces: + config: + - name: Ethernet12 + access: + vlan: 12 + trunk: + allowed_vlans: + - vlan: 13-14 + - name: Ethernet14 + access: + vlan: 10 + trunk: + allowed_vlans: + - vlan: 11 + - vlan: 13-14 + state: replaced + +# After state: +# ------------ +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Ethernet13 +# A Ethernet14 +#11 Inactive T Ethernet13 +# T Ethernet14 +#12 Inactive A Ethernet12 +#13 Inactive T Ethernet12 +# T Ethernet14 +#14 Inactive T Ethernet12 +# T Ethernet14 +# +# +# Using overridden +# +# Before state: +# ------------- +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive A Ethernet11 +#11 Inactive T Ethernet11 +#12 Inactive A Ethernet12 +#13 Inactive T Ethernet12 + +- name: Override L2 interfaces configuration in device with provided configuration + sonic_l2_interfaces: + config: + - name: Ethernet13 + access: + vlan: 12 + trunk: + allowed_vlans: + - vlan: 13-14 + state: overridden + +# After state: +# ------------ +# +#do show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#12 Inactive A Ethernet13 +#13 Inactive T Ethernet13 +#14 Inactive T Ethernet13 +# +# """ RETURN = """ before: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_acls.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_acls.py new file mode 100644 index 000000000..ad34025df --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_acls.py @@ -0,0 +1,1058 @@ +#!/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_l3_acls +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_l3_acls +version_added: '2.1.0' +notes: + - Supports C(check_mode). +short_description: Manage Layer 3 access control lists (ACL) configurations on SONiC +description: + - This module provides configuration management of Layer 3 access control lists (ACL) + in devices running SONiC. +author: 'Arun Saravanan Balachandran (@ArunSaravananBalachandran)' +options: + config: + description: + - Specifies Layer 3 ACL configurations. + type: list + elements: dict + suboptions: + address_family: + description: + - Specifies the address family of the ACLs. + type: str + required: true + choices: + - ipv4 + - ipv6 + acls: + description: + - List of ACL configuration for the given address family. + type: list + elements: dict + suboptions: + name: + description: + - Specifies the ACL name. + type: str + required: true + remark: + description: + - Specifies remark for the ACL. + type: str + rules: + description: + - List of rules with the ACL. + - I(sequence_num), I(action), I(protocol), I(source) & I(destination) are required for adding a new rule. + - If I(state=deleted), options other than I(sequence_num) are not considered. + type: list + elements: dict + suboptions: + sequence_num: + description: + - Specifies the sequence number of the rule. + - The range is from 1 to 65535. + type: int + required: true + action: + description: + - Specifies the action taken on the matched packet. + type: str + choices: + - deny + - discard + - do-not-nat + - permit + - transit + protocol: + description: + - Specifies the protocol to match. + - Only one suboption can be specified for protocol in a rule. + type: dict + suboptions: + name: + description: + - Match packets with the given protocol. + - C(ip) - Match any IPv4 packets. + - C(ipv6) - Match any IPv6 packets. + - C(icmp) - Match ICMP packets. + - C(icmpv6) - Match ICMPv6 packets. + - C(tcp) - Match TCP packets. + - C(udp) - Match UDP packets. + - C(ip) and C(icmp) are valid only for IPv4 ACLs. + - C(ipv6) and C(icmpv6) are valid only for IPv6 ACLs. + type: str + choices: + - ip + - ipv6 + - icmp + - icmpv6 + - tcp + - udp + number: + description: + - Match packets with given protocol number. + - The range is from 0 to 255. + type: int + source: + description: + - Specifies the source of the packet. + - I(any), I(host) and I(prefix) are mutually exclusive. + type: dict + suboptions: + any: + description: + - Match any source network address. + type: bool + host: + description: + - Network address of a single source host. + type: str + prefix: + description: + - Source network prefix in the format A.B.C.D/mask (ipv4) or A::B/mask (ipv6). + type: str + port_number: + description: + - Specifies the source port (valid only for TCP or UDP) + - Only one suboption can be specified for port_number in a rule. + type: dict + suboptions: + eq: + description: + - Match packets with source port equal to the given port number. + - The range is from 0 to 65535. + type: int + gt: + description: + - Match packets with source port greater than the given port number. + - The range is from 0 to 65534. + type: int + lt: + description: + - Match packets with source port lesser than the given port number. + - The range is from 1 to 65535. + type: int + range: + description: + - Match packets with source port in the given range. + - I(begin) and I(end) are required together. + type: dict + suboptions: + begin: + description: + - Specifies the beginning of the port range. + - The range is from 0 to 65534. + type: int + end: + description: + - Specifies the end of the port range. + - The range is from 1 to 65535. + type: int + destination: + description: + - Specifies the destination of the packet. + - I(any), I(host) and I(prefix) are mutually exclusive. + type: dict + suboptions: + any: + description: + - Match any destination network address. + type: bool + host: + description: + - Network address of a single destination host. + type: str + prefix: + description: + - Destination network prefix in the format A.B.C.D/mask (ipv4) or A::B/mask (ipv6). + type: str + port_number: + description: + - Specifies the destination port (valid only for TCP or UDP) + - Only one suboption can be specified for port_number in a rule. + type: dict + suboptions: + eq: + description: + - Match packets with destination port equal to the given port number. + - The range is from 0 to 65535. + type: int + gt: + description: + - Match packets with destination port greater than the given port number. + - The range is from 0 to 65534. + type: int + lt: + description: + - Match packets with destination port lesser than the given port number. + - The range is from 1 to 65535. + type: int + range: + description: + - Match packets with destination port in the given range. + - I(begin) and I(end) are required together. + type: dict + suboptions: + begin: + description: + - Specifies the beginning of the port range. + - The range is from 0 to 65534. + type: int + end: + description: + - Specifies the end of the port range. + - The range is from 1 to 65535. + type: int + protocol_options: + description: + - Specifies the additional packet match options for the chosen protocol. + - I(icmp), I(icmpv6) and I(tcp) are mutually exclusive. + type: dict + suboptions: + icmp: + description: + - Packet match options for ICMP. + type: dict + suboptions: + code: + description: + - Match packets with given ICMP code. + - The range is from 0 to 255. + type: int + type: + description: + - Match packets with given ICMP type. + - The range is from 0 to 255. + type: int + icmpv6: + description: + - Packet match options for ICMPv6. + type: dict + suboptions: + code: + description: + - Match packets with given ICMPv6 code. + - The range is from 0 to 255. + type: int + type: + description: + - Match packets with given ICMPv6 type. + - The range is from 0 to 255. + type: int + tcp: + description: + - Packet match options for TCP. + - I(established) and other TCP flag options are mutually exclusive. + type: dict + suboptions: + established: + description: + - Match packets which are part of established TCP session. + type: bool + ack: + description: + - Match packets with ACK flag set. + type: bool + not_ack: + description: + - Match packets with ACK flag cleared. + type: bool + fin: + description: + - Match packets with FIN flag set. + type: bool + not_fin: + description: + - Match packets with FIN flag cleared. + type: bool + psh: + description: + - Match packets with PSH flag set. + type: bool + not_psh: + description: + - Match packets with PSH flag cleared. + type: bool + rst: + description: + - Match packets with RST flag set. + type: bool + not_rst: + description: + - Match packets with RST flag cleared. + type: bool + syn: + description: + - Match packets with SYN flag set. + type: bool + not_syn: + description: + - Match packets with SYN flag cleared. + type: bool + urg: + description: + - Match packets with URG flag set. + type: bool + not_urg: + description: + - Match packets with URG flag cleared. + type: bool + vlan_id: + description: + - Match packets with the given VLAN ID value. + type: int + dscp: + description: + - Match packets using DSCP value. + - Only one suboption can be specified for dscp in a rule. + type: dict + suboptions: + value: + description: + - Match packets with given DSCP value. + - The range is from 0 to 63. + type: int + af11: + description: + - Match packets with AF11 DSCP (001010 - Decimal value 10). + type: bool + af12: + description: + - Match packets with AF12 DSCP (001100 - Decimal value 12). + type: bool + af13: + description: + - Match packets with AF13 DSCP (001110 - Decimal value 14). + type: bool + af21: + description: + - Match packets with AF21 DSCP (010010 - Decimal value 18). + type: bool + af22: + description: + - Match packets with AF22 DSCP (010100 - Decimal value 20). + type: bool + af23: + description: + - Match packets with AF23 DSCP (010110 - Decimal value 22). + type: bool + af31: + description: + - Match packets with AF31 DSCP (011010 - Decimal value 26). + type: bool + af32: + description: + - Match packets with AF32 DSCP (011100 - Decimal value 28). + type: bool + af33: + description: + - Match packets with AF33 DSCP (011110 - Decimal value 30). + type: bool + af41: + description: + - Match packets with AF41 DSCP (100010 - Decimal value 34). + type: bool + af42: + description: + - Match packets with AF42 DSCP (100100 - Decimal value 36). + type: bool + af43: + description: + - Match packets with AF43 DSCP (100110 - Decimal value 38). + type: bool + cs1: + description: + - Match packets with CS1 DSCP (001000 - Decimal value 8). + type: bool + cs2: + description: + - Match packets with CS2 DSCP (010000 - Decimal value 16). + type: bool + cs3: + description: + - Match packets with CS3 DSCP (011000 - Decimal value 24). + type: bool + cs4: + description: + - Match packets with CS4 DSCP (100000 - Decimal value 32). + type: bool + cs5: + description: + - Match packets with CS5 DSCP (101000 - Decimal value 40). + type: bool + cs6: + description: + - Match packets with CS6 DSCP (110000 - Decimal value 48). + type: bool + cs7: + description: + - Match packets with CS7 DSCP (111000 - Decimal value 56). + type: bool + default: + description: + - Match packets with CS0 DSCP (000000 - Decimal value 0). + type: bool + ef: + description: + - Match packets with EF DSCP (101110 - Decimal value 46). + type: bool + voice_admit: + description: + - Match packets with VOICE-ADMIT DSCP (101100 - Decimal value 44). + type: bool + remark: + description: + - Specifies remark for the ACL rule. + type: str + state: + description: + - The state of the configuration after module completion. + - C(merged) - Merges provided L3 ACL configuration with on-device configuration. + - C(replaced) - Replaces on-device configuration of the specified L3 ACLs with provided configuration. + - C(overridden) - Overrides all on-device L3 ACL configurations with the provided configuration. + - C(deleted) - Deletes on-device L3 ACL configuration. + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# Using merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit ipv6 host 192:168:1::2 any +# sonic# + + - name: Merge provided Layer 3 ACL configurations + dellemc.enterprise_sonic.sonic_l3_acls: + config: + - address_family: 'ipv4' + acls: + - name: 'test' + rules: + - sequence_num: 2 + action: 'permit' + protocol: + name: 'icmp' + source: + any: true + destination: + host: '192.168.1.2' + protocol_options: + icmp: + type: 8 + - sequence_num: 3 + action: 'deny' + protocol: + number: 2 + source: + any: true + destination: + any: true + - sequence_num: 4 + action: 'deny' + protocol: + name: 'ip' + source: + any: true + destination: + any: true + vlan_id: 10 + remark: 'Vlan10' + - name: 'test1' + remark: 'test_ip_acl' + rules: + - sequence_num: 1 + action: 'permit' + protocol: + name: 'tcp' + source: + prefix: '10.0.0.0/8' + destination: + any: true + - sequence_num: 2 + action: 'deny' + protocol: + name: 'udp' + source: + any: true + destination: + prefix: '20.1.0.0/16' + port_number: + gt: 1024 + - sequence_num: 3 + action: 'deny' + protocol: + name: 'ip' + source: + any: true + destination: + any: true + dscp: + value: 63 + - address_family: 'ipv6' + acls: + - name: 'testv6' + rules: + - sequence_num: 2 + action: 'deny' + protocol: + name: 'icmpv6' + source: + any: true + destination: + any: true + - name: 'testv6-1' + remark: 'test_ipv6_acl' + rules: + - sequence_num: 1 + action: 'permit' + protocol: + name: 'ipv6' + source: + prefix: '1000::/16' + destination: + any: true + dscp: + af22: true + - sequence_num: 2 + action: 'deny' + protocol: + name: 'tcp' + source: + any: true + destination: + prefix: '2000::1000:0/112' + port_number: + range: + begin: 100 + end: 1000 + - sequence_num: 3 + action: 'permit' + protocol: + name: 'tcp' + source: + any: true + destination: + any: true + protocol_options: + tcp: + established: true + - sequence_num: 4 + action: 'deny' + protocol: + name: 'udp' + source: + any: true + port_number: + eq: 3000 + destination: + any: true + state: merged + +# After State: +# ------------ +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 2 permit icmp any host 192.168.1.2 type 8 +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit ipv6 host 192:168:1::2 any +# seq 2 deny icmpv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + +# Using replaced +# +# Before State: +# ------------- +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 2 permit icmp any host 192.168.1.2 type 8 +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp host 3000::1 any established +# seq 2 permit udp any any +# seq 3 deny icmpv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + - name: Replace device configuration of specified Layer 3 ACLs with provided configuration + dellemc.enterprise_sonic.sonic_l3_acls: + config: + - address_family: 'ipv4' + acls: + - name: 'test2' + rules: + - sequence_num: 1 + action: 'permit' + protocol: + name: 'tcp' + source: + prefix: '192.168.1.0/24' + destination: + any: true + - address_family: 'ipv6' + acls: + - name: 'testv6' + rules: + - sequence_num: 1 + action: 'permit' + protocol: + name: 'tcp' + source: + host: '3000::1' + destination: + any: true + protocol_options: + tcp: + ack: true + syn: true + fin: true + - sequence_num: 2 + action: 'deny' + protocol: + name: 'ipv6' + source: + any: true + destination: + any: true + state: replaced + +# After State: +# ------------ +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 2 permit icmp any host 192.168.1.3 type 8 +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# ! +# ip access-list test2 +# seq 1 permit tcp 192.168.1.0/24 any +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp host 3000::1 any fin syn ack +# seq 2 deny ipv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 2 permit icmp any host 192.168.1.3 type 8 +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# ! +# ip access-list test2 +# seq 1 permit tcp 192.168.1.0/24 any +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp 3000::/16 any +# seq 2 deny ipv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + - name: Override device configuration of all Layer 3 ACLs with provided configuration + dellemc.enterprise_sonic.sonic_l3_acls: + config: + - address_family: 'ipv4' + acls: + - name: 'test_acl' + rules: + - sequence_num: 1 + action: 'permit' + protocol: + name: 'ip' + source: + prefix: '100.1.1.0/24' + destination: + prefix: '100.1.2.0/24' + - sequence_num: 2 + action: 'deny' + protocol: + name: 'udp' + source: + any: true + destination: + any: true + state: overridden + +# After State: +# ------------ +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test_acl +# seq 1 permit ip 100.1.1.0/24 100.1.2.0/24 +# seq 2 deny udp any any +# sonic# +# sonic# show running-configuration ipv6 access-list +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 2 permit icmp any host 192.168.1.3 type 8 +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# ! +# ip access-list test2 +# seq 1 permit tcp 192.168.1.0/24 any +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp 3000::/16 any +# seq 2 deny ipv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + - name: Delete specified Layer 3 ACLs, ACL remark and ACL rule entries + dellemc.enterprise_sonic.sonic_l3_acls: + config: + - address_family: 'ipv4' + acls: + - name: 'test' + rules: + - sequence_num: 2 + - name: 'test2' + - address_family: 'ipv6' + acls: + - name: 'testv6-1' + remark: 'test_ipv6_acl' + rules: + - sequence_num: 3 + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp 3000::/16 any +# seq 2 deny ipv6 any any +# ! +# ipv6 access-list testv6-1 +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 4 deny udp any eq 3000 any +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 2 permit icmp any host 192.168.1.3 type 8 +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# ! +# ip access-list test2 +# seq 1 permit tcp 192.168.1.0/24 any +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp 3000::/16 any +# seq 2 deny ipv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + - name: Delete all Layer 3 ACLs for an address-family + dellemc.enterprise_sonic.sonic_l3_acls: + config: + - address_family: 'ipv4' + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration ip access-list +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp 3000::/16 any +# seq 2 deny ipv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration ip access-list +# ! +# ip access-list test +# seq 1 permit ip host 192.168.1.2 any +# seq 2 permit icmp any host 192.168.1.3 type 8 +# seq 3 deny 2 any any +# seq 4 deny ip any any vlan 10 remark Vlan10 +# ! +# ip access-list test1 +# remark test_ip_acl +# seq 1 permit tcp 10.0.0.0/8 any +# seq 2 deny udp any 20.1.0.0/16 gt 1024 +# seq 3 deny ip any any dscp 63 +# ! +# ip access-list test2 +# seq 1 permit tcp 192.168.1.0/24 any +# sonic# +# sonic# show running-configuration ipv6 access-list +# ! +# ipv6 access-list testv6 +# seq 1 permit tcp 3000::/16 any +# seq 2 deny ipv6 any any +# ! +# ipv6 access-list testv6-1 +# remark test_ipv6_acl +# seq 1 permit ipv6 1000::/16 any dscp af22 +# seq 2 deny tcp any 2000::1000:0/112 range 100 1000 +# seq 3 permit tcp any any established +# seq 4 deny udp any eq 3000 any +# sonic# + + - name: Delete all Layer 3 ACL configurations + dellemc.enterprise_sonic.sonic_l3_acls: + config: + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration ip access-list +# sonic# +# sonic# show running-configuration ipv6 access-list +# sonic# + + +""" +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. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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.l3_acls.l3_acls import L3_aclsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.l3_acls.l3_acls import L3_acls + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=L3_aclsArgs.argument_spec, + supports_check_mode=True) + + result = L3_acls(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 index e796897a5..1ebc11994 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_interfaces.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_l3_interfaces.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# Copyright 2023 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) @@ -29,6 +29,13 @@ The module file for sonic_l3_interfaces from __future__ import absolute_import, division, print_function __metaclass__ = type +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community', + 'license': 'Apache 2.0' +} + DOCUMENTATION = """ --- module: sonic_l3_interfaces @@ -44,7 +51,8 @@ description: author: Kumaraguru Narayanan (@nkumaraguru) options: config: - description: A list of l3_interfaces configurations. + description: + - A list of l3_interfaces configurations. type: list elements: dict suboptions: @@ -101,11 +109,13 @@ options: type: bool state: description: - - The state that the configuration should be left in. + - The state of the configuration after module completion. type: str choices: - - merged - - deleted + - merged + - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -328,7 +338,181 @@ EXAMPLES = """ # ip anycast-address 11.12.13.14/12 #! # +# Using replaced +# +# 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 +#! +# +- name: Replace l3 interface + dellemc.enterprise_sonic.sonic_l3_interfaces: + config: + - name: Ethernet20 + ipv4: + - address: 81.1.1.1/16 + state: replaced + +# After state: +# ------------ # +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 81.1.1.1/16 +#! +#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 +#! +# +# Using replaced +# +# 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 +#! +- name: Replace l3 interface + dellemc.enterprise_sonic.sonic_l3_interfaces: + config: + - name: Ethernet20 + state: replaced + +# After state: +# ------------ +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +#! +#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 +#! +# +# Using overridden +# +# 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 +#! +# +- name: Override l3 interface + dellemc.enterprise_sonic.sonic_l3_interfaces: + config: + - name: Ethernet24 + ipv4: + - address: 81.1.1.1/16 + - name: Vlan100 + ipv4: + anycast_addresses: + - 83.1.1.1/24 + - 85.1.1.12/24 + state: overridden + +# After state: +# ------------ +# +#rno-dctor-1ar01c01sw02# show running-configuration interface +#! +#interface Ethernet20 +# mtu 9100 +# speed 100000 +# shutdown +#! +#interface Ethernet24 +# mtu 9100 +# speed 100000 +# shutdown +# ip address 81.1.1.1/16 +#! +#interface Vlan100 +# ip anycast-address 83.1.1.1/24 +# ip anycast-address 85.1.1.12/24 +#! + + """ RETURN = """ before: @@ -336,14 +520,14 @@ before: returned: always type: list sample: > - The configuration returned is always in the same format + 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 is always in the same format + 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. 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 index 630db7985..0fd0d8b7e 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lag_interfaces.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lag_interfaces.py @@ -80,6 +80,8 @@ options: type: str choices: - merged + - replaced + - overridden - deleted default: merged """ @@ -124,6 +126,111 @@ EXAMPLES = """ # speed 100000 # no shutdown # +# Using replaced +# +# Before state: +# ------------- +# +# interface Eth1/5 +# channel-group 10 +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/6 +# channel-group 20 +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/7 +# no channel-group +# mtu 9100 +# speed 100000 +# no shutdown +# +- name: Replace device configuration of specified LAG attributes + dellemc.enterprise_sonic.sonic_lag_interfaces: + config: + - name: PortChannel10 + members: + interfaces: + - member: Eth1/7 + state: replaced +# +# After state: +# ------------ +# +# interface Eth1/5 +# no channel-group +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/6 +# channel-group 20 +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/7 +# channel-group 10 +# mtu 9100 +# speed 100000 +# no shutdown +# +# Using overridden +# +# Before state: +# ------------- +# +# interface Eth1/5 +# channel-group 10 +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/6 +# no channel-group +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/7 +# channel-group 2 +# mtu 9100 +# speed 100000 +# no shutdown +# +- name: Override device configuration of all LAG attributes + dellemc.enterprise_sonic.sonic_lag_interfaces: + config: + - name: PortChannel20 + members: + interfaces: + - member: Eth1/6 + state: overridden +# +# After state: +# ------------ +# interface Eth1/5 +# no channel-group +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/6 +# channel-group 20 +# mtu 9100 +# speed 100000 +# no shutdown +# +# interface Eth1/7 +# no channel-group +# mtu 9100 +# speed 100000 +# no shutdown +# # Using deleted # # Before state: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lldp_global.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lldp_global.py new file mode 100644 index 000000000..6577d21d0 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_lldp_global.py @@ -0,0 +1,301 @@ +#!/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_lldp_global +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_lldp_global +version_added: '2.1.0' +short_description: Manage Global LLDP configurations on SONiC +description: + - This module provides configuration management of global LLDP parameters + for use on LLDP enabled Layer 2 interfaces of devices running SONiC. + - It is intended for use in conjunction with LLDP Layer 2 interface + configuration applied on participating interfaces. +author: 'Divya Balasubramanian(@divya-balasubramania)' +options: + config: + description: The set of link layer discovery protocol global attribute configurations + type: dict + suboptions: + enable: + description: + - This argument is a boolean value to enable or disable LLDP. + type: bool + multiplier: + description: + - Multiplier value is used to determine the timeout interval (i.e. hello-time x multiplier value) + - The range is from 1 to 10 + type: int + system_description: + description: + - Description of this system to be sent in LLDP advertisements. + - When configured, this value is used in the advertisements + instead of the default system description. + type: str + system_name: + description: + - Specifying a descriptive system name using this command, user may find it easier to distinguish the device with LLDP. + - By default, the host name is used. + type: str + mode: + description: + - By default both transmit and receive of LLDP frames is enabled. + - This command can be used to configure either in receive only or transmit only mode. + type: str + choices: + - receive + - transmit + hello_time: + description: + - Frequency at which LLDP advertisements are sent (in seconds). + - The range is from 5 to 254 sec + type: int + tlv_select: + description: + - By default, management address and system capabilities TLV are advertised in LLDP frames. + - This configuration option can be used to selectively suppress sending of these TLVs + to the Peer. + type: dict + suboptions: + management_address: + description: + - Enable or disable management address TLV. + type: bool + system_capabilities: + description: + - Enable or disable system capabilities TLV. + type: bool + state: + description: + - The state specifies the type of configuration update to be performed on the device. + - If the state is "merged", merge specified attributes with existing configured attributes. + - For "deleted", delete the specified attributes from existing configuration. + type: str + choices: + - merged + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration +# ! +# lldp receive +# lldp timer 200 +# lldp multiplier 1 +# lldp system-name 8999_System +# lldp system-description sonic_system +# ! + + - name: Delete LLDP configurations + dellemc.enterprise_sonic.sonic_lldp_global: + config: + hello_time: 200 + system_description : sonic_system + mode: receive + multiplier: 1 + state: deleted + +# After State: +# ------------ +# sonic# show running-configuration | grep lldp +# ! +# lldp system-name 8999_System +# ! +# sonic# + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep lldp +# sonic# + + - name: Delete default LLDP configurations + dellemc.enterprise_sonic.sonic_lldp_global: + config: + tlv_select: + system_capabilities: true + state: deleted + +# After State: +# ------------ +# sonic# show running-configuration +# ! +# no lldp tlv-select system-capabilities +# ! + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep lldp +# ! +# lldp receive +# lldp timer 200 +# lldp multiplier 1 +# lldp system-name 8999_System +# lldp system-description sonic_system +# ! + + - name: Delete all LLDP configuration + dellemc.enterprise_sonic.sonic_lldp_global: + config: + state: deleted + +# After State: (No LLDP global configuration present.) +# ------------ +# sonic# show running-configuration | grep lldp +# sonic# + + +# Using Merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep lldp +# sonic# + + - name: Modify LLDP configurations + dellemc.enterprise_sonic.sonic_lldp_global: + config: + enable: false + multiplier: 9 + system_name : CR_sonic + hello_time: 18 + mode: receive + system_description: Sonic_System + tlv_select: + management_address: true + system_capabilities: false + state: merged + +# After State: +# ------------ +# sonic# show running-configuration | grep lldp +# ! +# no lldp enable +# no lldp tlv-select system_capabilities +# lldp receive +# lldp timer 18 +# lldp multiplier 9 +# lldp system-name CR_sonic +# lldp system-description Sonic_System +# ! + + +# Using Merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep lldp +# ! +# lldp receive +# lldp timer 200 +# lldp multiplier 1 +# lldp system-name 8999_System +# lldp system-description sonic_system +# ! + + - name: Modify LLDP configurations + dellemc.enterprise_sonic.sonic_lldp_global: + config: + multiplier: 9 + system_name : CR_sonic + state: merged + +# After State: +# ------------ +# sonic# show running-configuration | grep lldp +# ! +# lldp receive +# lldp timer 200 +# lldp multiplier 9 +# lldp system-name CR_sonic +# lldp system-description sonic_system +# ! + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + sample: > + The configuration returned will always be in the same format + of the parameters above. + type: list +after: + description: The resulting configuration model invocation. + returned: when changed + sample: > + The configuration returned will always be in the same format + of the parameters above. + type: list +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.lldp_global.lldp_global import Lldp_globalArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.lldp_global.lldp_global import Lldp_global + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Lldp_globalArgs.argument_spec, + supports_check_mode=True) + + result = Lldp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_logging.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_logging.py new file mode 100644 index 000000000..d8c592874 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_logging.py @@ -0,0 +1,274 @@ +#!/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_logging +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_logging +version_added: 2.1.0 +notes: + - Supports C(check_mode). +short_description: Manage logging configuration on SONiC. +description: + - This module provides configuration management of logging for devices running SONiC. +author: "M. Zhang (@mingjunzhang2019)" +options: + config: + description: + - Specifies logging related configurations. + type: dict + suboptions: + remote_servers: + type: list + elements: dict + description: + - Remote logging sever configuration. + suboptions: + host: + type: str + description: + - IPv4/IPv6 address or host name of the remote logging server. + required: true + remote_port: + type: int + description: + - Destination port number for logging messages sent to the server. + - remote_port can not be deleted. + source_interface: + type: str + description: + - Source interface used as source ip for sending logging packets. + - source_interface can not be deleted. + message_type: + type: str + description: + - Type of messages that remote server receives. + - message_type can not be deleted. + choices: + - log + - event + vrf: + type: str + description: + - VRF name used by remote logging server. + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.0.2 5 Ethernet24 - event +#10.11.1.1 616 Ethernet8 - log +#log1.dell.com 6 Ethernet28 - log +# +- name: Delete logging server configuration + sonic_logging: + config: + remote_servers: + - host: 10.11.0.2 + - host: log1.dell.com + state: deleted + +# After state: +# ------------ +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.1.1 616 Ethernet8 - log +# +# +# Using merged +# +# Before state: +# ------------- +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.1.1 616 Ethernet8 - log +# +- name: Merge logging server configuration + sonic_logging: + config: + remote_servers: + - host: 10.11.0.2 + remote_port: 5 + source_interface: Ethernet24 + message_type: event + - host: log1.dell.com + remote_port: 6 + source_interface: Ethernet28 + state: merged + +# After state: +# ------------ +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.0.2 5 Ethernet24 - event +#10.11.1.1 616 Ethernet8 - log +#log1.dell.com 6 Ethernet28 - log +# +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.1.1 616 Ethernet8 - log +#10.11.1.2 626 Ethernet16 - event +# +- name: Replace logging server configuration + sonic_logging: + config: + remote_servers: + - host: 10.11.1.2 + remote_port: 622 + source_interface: Ethernet24 + message_type: event + state: overridden +# +# After state: +# ------------ +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.1.2 622 Ethernet24 - event +# +# Using replaced +# +# Before state: +# ------------- +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.1.1 616 Ethernet8 - log +#10.11.1.2 626 Ethernet16 - event +# +- name: Replace logging server configuration + sonic_logging: + config: + remote_servers: + - host: 10.11.1.2 + remote_port: 622 + state: replaced +# +# After state: +# ------------ +# +# "MESSAGE-TYPE" has default value of "log" +# +#sonic# show logging servers +#-------------------------------------------------------------------------------- +#HOST PORT SOURCE-INTERFACE VRF MESSGE-TYPE +#-------------------------------------------------------------------------------- +#10.11.1.1 616 Ethernet8 - log +#10.11.1.2 622 - - log +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + type: list + returned: always + 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. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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.logging.logging import LoggingArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.logging.logging import Logging + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=LoggingArgs.argument_spec, + supports_check_mode=True) + + result = Logging(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mac.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mac.py new file mode 100644 index 000000000..42acfd4fb --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mac.py @@ -0,0 +1,319 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 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_mac +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = """ +--- +module: sonic_mac +version_added: "2.1.0" +short_description: Manage MAC configuration on SONiC +description: + - This module provides configuration management of MAC for devices running SONiC +author: "Shade Talabi (@stalabi1)" +options: + config: + description: + - A list of MAC configurations. + type: list + elements: dict + suboptions: + vrf_name: + description: + - Specifies the VRF name. + type: str + default: 'default' + mac: + description: + - Configuration attributes for MAC. + type: dict + suboptions: + aging_time: + description: + - Time in seconds of inactivity before the MAC entry is timed out. + type: int + default: 600 + dampening_interval: + description: + - Interval for which mac movements are observed before disabling MAC learning on a port. + type: int + default: 5 + dampening_threshold: + description: + - Number of MAC movements allowed per second before disabling MAC learning on a port. + type: int + default: 5 + mac_table_entries: + description: + - Configuration attributes for MAC table entries. + type: list + elements: dict + suboptions: + mac_address: + description: + - MAC address for the dynamic or static MAC table entry. + type: str + required: True + vlan_id: + description: + - ID number of VLAN on which the MAC address is present. + type: int + required: True + interface: + description: + - Specifies the interface for the MAC table entry. + type: str + state: + description: + - The state of the configuration after module completion + type: str + choices: ['merged', 'deleted', 'replaced', 'overridden'] + default: merged +""" +EXAMPLES = """ +# Using merged +# +# Before state: +# ------------- +# +# sonic# show mac dampening +# MAC Move Dampening Threshold : 5 +# MAC Move Dampening Interval : 5 +# sonic# show running-configuration | grep mac +# (No mac configuration pressent) + + - name: Merge MAC configurations + dellemc.enterprise_sonic.sonic_mac: + config: + - vrf_name: 'default' + mac: + aging_time: 50 + dampening_interval: 20 + dampening_threshold: 30 + mac_table_entries: + - mac_address: '00:00:5e:00:53:af' + vlan_id: 1 + interface: 'Ethernet20' + - mac_address: '00:33:33:33:33:33' + vlan_id: 2 + interface: 'Ethernet24' + - mac_address: '00:00:4e:00:24:af' + vlan_id: 3 + interface: 'Ethernet28' + state: merged + +# After state: +# ------------ +# +# sonic# show mac dampening +# MAC Move Dampening Threshold : 30 +# MAC Move Dampening Interval : 20 +# sonic# show running-configuration | grep mac +# mac address-table 00:00:5e:00:53:af Vlan1 Ethernet20 +# mac address-table 00:33:33:33:33:33 Vlan2 Ethernet24 +# mac address-table 00:00:4e:00:24:af Vlan3 Ethernet28 +# mac address-table aging-time 50 +# +# +# Using replaced +# +# Before state: +# ------------- +# +# sonic# show mac dampening +# MAC Move Dampening Threshold : 30 +# MAC Move Dampening Interval : 20 +# sonic# show running-configuration | grep mac +# mac address-table 00:00:5e:00:53:af Vlan1 Ethernet20 +# mac address-table 00:33:33:33:33:33 Vlan2 Ethernet24 +# mac address-table 00:00:4e:00:24:af Vlan3 Ethernet28 +# mac address-table aging-time 50 + + - name: Replace MAC configurations + dellemc.enterprise_sonic.sonic_mac: + config: + - vrf_name: 'default' + mac: + aging_time: 45 + dampening_interval: 30 + dampening_threshold: 60 + mac_table_entries: + - mac_address: '00:00:5e:00:53:af' + vlan_id: 3 + interface: 'Ethernet24' + - mac_address: '00:44:44:44:44:44' + vlan_id: 2 + interface: 'Ethernet20' + state: replaced + +# sonic# show mac dampening +# MAC Move Dampening Threshold : 60 +# MAC Move Dampening Interval : 30 +# sonic# show running-configuration | grep mac +# mac address-table 00:00:5e:00:53:af Vlan3 Ethernet24 +# mac address-table 00:33:33:33:33:33 Vlan2 Ethernet24 +# mac address-table 00:00:4e:00:24:af Vlan3 Ethernet28 +# mac address-table 00:44:44:44:44:44 Vlan2 Ethernet20 +# mac address-table aging-time 45 +# +# +# Using overridden +# +# Before state: +# ------------- +# +# sonic# show mac dampening +# MAC Move Dampening Threshold : 60 +# MAC Move Dampening Interval : 30 +# sonic# show running-configuration | grep mac +# mac address-table 00:00:5e:00:53:af Vlan3 Ethernet24 +# mac address-table 00:33:33:33:33:33 Vlan2 Ethernet24 +# mac address-table 00:00:4e:00:24:af Vlan3 Ethernet28 +# mac address-table 00:44:44:44:44:44 Vlan2 Ethernet20 +# mac address-table aging-time 45 + + - name: Override MAC cofigurations + dellemc.enterprise_sonic.sonic_mac: + config: + - vrf_name: 'default' + mac: + aging_time: 10 + dampening_interval: 20 + dampening_threshold: 30 + mac_table_entries: + - mac_address: '00:11:11:11:11:11' + vlan_id: 1 + interface: 'Ethernet20' + - mac_address: '00:22:22:22:22:22' + vlan_id: 2 + interface: 'Ethernet24' + state: overridden + +# After state: +# ------------ +# +# sonic# show mac dampening +# MAC Move Dampening Threshold : 30 +# MAC Move Dampening Interval : 20 +# sonic# show running-configuration | grep mac +# mac address-table 00:11:11:11:11:11 Vlan1 Ethernet20 +# mac address-table 00:22:22:22:22:22 Vlan2 Ethernet24 +# mac address-table aging-time 10 +# +# +# Using deleted +# +# Before state: +# ------------- +# +# sonic# show mac dampening +# MAC Move Dampening Threshold : 30 +# MAC Move Dampening Interval : 20 +# sonic# show running-configuration | grep mac +# mac address-table 00:11:11:11:11:11 Vlan1 Ethernet20 +# mac address-table 00:22:22:22:22:22 Vlan2 Ethernet24 +# mac address-table aging-time 10 + + - name: Delete MAC cofigurations + dellemc.enterprise_sonic.sonic_mac: + config: + - vrf_name: 'default' + mac: + aging_time: 10 + dampening_interval: 20 + dampening_threshold: 30 + mac_table_entries: + - mac_address: '00:11:11:11:11:11' + vlan_id: 1 + interface: 'Ethernet20' + - mac_address: '00:22:22:22:22:22' + vlan_id: 2 + interface: 'Ethernet24' + state: deleted + +# After state: +# ------------ +# +# sonic# show mac dampening +# MAC Move Dampening Threshold : 5 +# MAC Move Dampening Interval : 5 +# sonic# show running-configuration | grep mac +# (No mac configuration pressent) + + +""" +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.mac.mac import MacArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.mac.mac import Mac + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=MacArgs.argument_spec, + supports_check_mode=True) + + result = Mac(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 index 28d3dbb5b..e17fe080c 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mclag.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_mclag.py @@ -34,11 +34,11 @@ DOCUMENTATION = """ module: sonic_mclag version_added: 1.0.0 notes: -- Tested against Enterprise SONiC Distribution by Dell Technologies. -- Supports C(check_mode). + - 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 + - Manage multi chassis link aggregation groups domain (MCLAG) and its parameters. author: Abirami N (@abirami-n) options: @@ -65,7 +65,7 @@ options: type: str system_mac: description: - - Mac address of MCLAG. + - MAC address of MCLAG. type: str keepalive: description: @@ -75,17 +75,45 @@ options: description: - MCLAG session timeout value in secs. type: int + delay_restore: + description: + - MCLAG delay restore time in secs. + type: int + gateway_mac: + description: + - Gateway MAC address for router ports over MCLAG. + - Configured gateway MAC address can be modified only when I(state=replaced) or I(state=overridden). + type: str unique_ip: - description: Holds Vlan dictionary for mclag unique ip. + description: Holds Vlan dictionary for MCLAG unique IP. + suboptions: + vlans: + description: + - Holds a list of VLANs and VLAN ranges for which a separate IP address is enabled for Layer 3 protocol support over MCLAG. + type: list + elements: dict + suboptions: + vlan: + description: + - Holds a VLAN name or VLAN range. + - Specify a single VLAN eg. Vlan10. + - Specify a range of VLANs eg. Vlan10-20. + type: str + type: dict + peer_gateway: + description: Holds Vlan dictionary for MCLAG peer gateway. suboptions: vlans: description: - - Holds list of VLANs for which a separate IP addresses is enabled for Layer 3 protocol support over MCLAG. + - Holds a list of VLANs and VLAN ranges for which MCLAG peer gateway functionality is enabled. type: list elements: dict suboptions: vlan: - description: Holds a VLAN ID. + description: + - Holds a VLAN name or VLAN range. + - Specify a single VLAN eg. Vlan10. + - Specify a range of VLANs eg. Vlan10-20. type: str type: dict members: @@ -106,8 +134,10 @@ options: - The state that the configuration should be left in. type: str choices: - - merged - - deleted + - merged + - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -118,7 +148,7 @@ EXAMPLES = """ # # sonic# show mclag brief # MCLAG Not Configured -# + - name: Merge provided configuration with device configuration dellemc.enterprise_sonic.sonic_mclag: config: @@ -128,14 +158,22 @@ EXAMPLES = """ peer_link: 'Portchannel1' keepalive: 1 session_timeout: 3 + delay_restore: 240 + system_mac: '00:00:00:11:11:11' + gateway_mac: '00:00:00:12:12:12' unique_ip: - vlans: - - vlan: Vlan4 + vlans: + - vlan: Vlan4 + - vlan: Vlan21-25 + peer_gateway: + vlans: + - vlan: Vlan4 + - vlan: Vlan21-25 members: - portchannles: - - lag: PortChannel10 + portchannels: + - lag: PortChannel10 state: merged -# + # After state: # ------------ # @@ -150,7 +188,10 @@ EXAMPLES = """ # Peer Link : PortChannel1 # Keepalive Interval : 1 secs # Session Timeout : 3 secs +# Delay Restore : 240 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:1 @@ -159,18 +200,34 @@ EXAMPLES = """ #----------------------------------------------------------- # PortChannel10 down/down # -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# } -# }, -# ... -# } -# -# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# + + # Using merged # # Before state: @@ -187,7 +244,10 @@ EXAMPLES = """ # Peer Link : PortChannel1 # Keepalive Interval : 1 secs # Session Timeout : 3 secs +# Delay Restore : 240 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:1 @@ -196,18 +256,33 @@ EXAMPLES = """ #----------------------------------------------------------- # PortChannel10 down/down # -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# } -# }, -# ... -# } -# -# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# + - name: Merge device configuration with the provided configuration dellemc.enterprise_sonic.sonic_mclag: config: @@ -215,14 +290,20 @@ EXAMPLES = """ source_address: 3.3.3.3 keepalive: 10 session_timeout: 30 + delay_restore: 360 unique_ip: vlans: - vlan: Vlan5 + - vlan: Vlan26-28 + peer_gateway: + vlans: + - vlan: Vlan5 + - vlan: Vlan26-28 members: portchannels: - lag: PortChannel12 state: merged -# + # After state: # ------------ # @@ -237,7 +318,10 @@ EXAMPLES = """ # Peer Link : PortChannel1 # Keepalive Interval : 10 secs # Session Timeout : 30 secs +# Delay Restore : 360 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:2 @@ -247,21 +331,41 @@ EXAMPLES = """ # PortChannel10 down/down # PortChannel12 down/down # -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# }, -# "Vlan5": { -# "unique_ip": "enable" -# } -# }, -# ... -# } -# -# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan5 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# Vlan26 +# Vlan27 +# Vlan28 +# ============== +# Total count : 10 +# ============== +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan5 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# Vlan26 +# Vlan27 +# Vlan28 +# ============== +# Total count : 10 +# ============== +# sonic# + + # Using deleted # # Before state: @@ -278,7 +382,10 @@ EXAMPLES = """ # Peer Link : PortChannel1 # Keepalive Interval : 10 secs # Session Timeout : 30 secs +# Delay Restore : 360 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:1 @@ -287,28 +394,52 @@ EXAMPLES = """ #----------------------------------------------------------- # PortChannel10 down/down # -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# } -# }, -# ... -# } -# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# + - 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 -# + config: + domain_id: 1 + source_address: 3.3.3.3 + keepalive: 10 + unique_ip: + vlans: + - vlan: Vlan22 + - vlan: Vlan24-25 + peer_gateway: + vlans: + - vlan: Vlan22 + - vlan: Vlan24-25 + members: + portchannels: + - lag: PortChannel10 + state: deleted + # After state: # ------------ # @@ -322,25 +453,37 @@ EXAMPLES = """ # Peer Address : 1.1.1.1 # Peer Link : PortChannel1 # Keepalive Interval : 1 secs -# Session Timeout : 15 secs +# Session Timeout : 30 secs +# Delay Restore : 360 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:0 # -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# } -# }, -# ... -# } -# -# -# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan23 +# ============== +# Total count : 3 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan23 +# ============== +# Total count : 3 +# ============== +# sonic# + + # Using deleted # # Before state: @@ -357,7 +500,10 @@ EXAMPLES = """ # Peer Link : PortChannel1 # Keepalive Interval : 10 secs # Session Timeout : 30 secs +# Delay Restore : 360 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:1 @@ -366,32 +512,40 @@ EXAMPLES = """ #----------------------------------------------------------- # PortChannel10 down/down # -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# } -# }, -# ... -# } -# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# ============== +# Total count : 1 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# ============== +# Total count : 1 +# ============== +# sonic# + - 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:~$ -# -# +# sonic# show mclag separate-ip-interfaces +# MCLAG separate IP interface not configured +# sonic# show mclag peer-gateway-interfaces +# MCLAG Peer Gateway interface not configured +# sonic# + + # Using deleted # # Before state: @@ -408,7 +562,10 @@ EXAMPLES = """ # Peer Link : PortChannel1 # Keepalive Interval : 10 secs # Session Timeout : 30 secs +# Delay Restore : 360 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:2 @@ -416,29 +573,37 @@ EXAMPLES = """ # MLAG Interface Local/Remote Status #----------------------------------------------------------- # PortChannel10 down/down -# PortChannel12 down/sown -# -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# } -# }, -# ... -# } +# PortChannel12 down/down +# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# ============== +# Total count : 1 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# ============== +# Total count : 1 +# ============== +# sonic# + - 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 + peer_gateway: + vlans: members: portchannels: - - lag: PortChannel10 state: deleted -# + # After state: # ------------ # @@ -452,24 +617,283 @@ EXAMPLES = """ # Peer Address : 1.1.1.1 # Peer Link : PortChannel1 # Keepalive Interval : 1 secs -# Session Timeout : 15 secs +# Session Timeout : 30 secs +# Delay Restore : 360 secs # System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 # # # Number of MLAG Interfaces:0 # -# admin@sonic:~$ show runningconfiguration all -# { -# ... -# "MCLAG_UNIQUE_IP": { -# "Vlan4": { -# "unique_ip": "enable" -# } -# }, -# ... -# } +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# ============== +# Total count : 1 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# MCLAG Peer Gateway interface not configured +# sonic# + + +# Using replaced +# +# 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 +# Delay Restore : 240 secs +# System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 +# +# +# Number of MLAG Interfaces:2 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# PortChannel11 down/down +# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# + +- name: Replace device configuration with the provided configuration + dellemc.enterprise_sonic.sonic_mclag: + config: + domain_id: 1 + unique_ip: + vlans: + - vlan: Vlan5 + - vlan: Vlan24-28 + peer_gateway: + vlans: + - vlan: Vlan5 + - vlan: Vlan24-28 + members: + portchannels: + - lag: PortChannel10 + - lag: PortChannel12 + state: replaced + +# 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 +# Delay Restore : 240 secs +# System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 +# +# +# Number of MLAG Interfaces:2 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# PortChannel12 down/down +# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan5 +# Vlan24 +# Vlan25 +# Vlan26 +# Vlan27 +# Vlan28 +# ============== +# Total count : 6 +# ============== +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan5 +# Vlan24 +# Vlan25 +# Vlan26 +# Vlan27 +# Vlan28 +# ============== +# Total count : 6 +# ============== +# sonic# + + +# Using overridden +# +# 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 +# Delay Restore : 240 secs +# System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 +# +# +# Number of MLAG Interfaces:2 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# PortChannel11 down/down +# +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan4 +# Vlan21 +# Vlan22 +# Vlan23 +# Vlan24 +# Vlan25 +# ============== +# Total count : 6 +# ============== +# sonic# + +- name: Override device configuration with the provided configuration + dellemc.enterprise_sonic.sonic_mclag: + config: + domain_id: 1 + peer_address: 1.1.1.1 + source_address: 3.3.3.3 + peer_link: 'Portchannel1' + system_mac: '00:00:00:11:11:11' + gateway_mac: '00:00:00:12:12:12' + unique_ip: + vlans: + - vlan: Vlan24-28 + peer_gateway: + vlans: + - vlan: Vlan24-28 + members: + portchannels: + - lag: PortChannel10 + - lag: PortChannel12 + state: overridden + +# 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 : 1 secs +# Session Timeout : 30 secs +# Delay Restore : 300 secs +# System Mac : 20:04:0f:37:bd:c9 +# Mclag System Mac : 00:00:00:11:11:11 +# Gateway Mac : 00:00:00:12:12:12 +# +# +# Number of MLAG Interfaces:2 +#----------------------------------------------------------- +# MLAG Interface Local/Remote Status +#----------------------------------------------------------- +# PortChannel10 down/down +# PortChannel12 down/down # +# sonic# show mclag separate-ip-interfaces +# Interface Name +# ============== +# Vlan24 +# Vlan25 +# Vlan26 +# Vlan27 +# Vlan28 +# ============== +# Total count : 5 +# ============== +# sonic# show mclag peer-gateway-interfaces +# Interface Name +# ============== +# Vlan24 +# Vlan25 +# Vlan26 +# Vlan27 +# Vlan28 +# ============== +# Total count : 5 +# ============== +# sonic# """ RETURN = """ before: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ntp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ntp.py index 87db8bb06..c04e437c1 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ntp.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_ntp.py @@ -34,6 +34,8 @@ DOCUMENTATION = """ --- module: sonic_ntp version_added: 2.0.0 +notes: + - Supports C(check_mode). short_description: Manage NTP configuration on SONiC. description: - This module provides configuration management of NTP for devices running SONiC. @@ -67,6 +69,7 @@ options: elements: dict description: - List of NTP servers. + - minpoll and maxpoll are required to be configured together. suboptions: address: type: str @@ -88,6 +91,11 @@ options: description: - Maximum poll interval to poll NTP server. - maxpoll can not be deleted. + prefer: + type: bool + description: + - Indicates whether this server should be preferred. + - prefer can not be deleted. ntp_keys: type: list elements: dict @@ -127,8 +135,10 @@ options: - The state of the configuration after module completion. type: str choices: - - merged - - deleted + - merged + - replaced + - overridden + - deleted default: merged """ EXAMPLES = """ @@ -138,16 +148,16 @@ EXAMPLES = """ # ------------- # #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 +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.1 6 10 False +#10.11.0.2 5 9 False +#dell.com 6 9 False +#dell.org 7 10 True # - name: Delete NTP server configuration - ntp: + sonic_ntp: config: servers: - address: 10.11.0.2 @@ -158,11 +168,11 @@ EXAMPLES = """ # ------------ # #sonic# show ntp server -#---------------------------------------------------------------------- -#NTP Servers minpoll maxpoll Authentication key ID -#---------------------------------------------------------------------- -#10.11.0.1 6 10 -#dell.com 6 9 +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.1 6 10 False +#dell.com 6 9 False # # # Using deleted @@ -177,7 +187,7 @@ EXAMPLES = """ #NTP source-interfaces: Ethernet0, Ethernet4, Ethernet8, Ethernet16 # - name: Delete NTP source-interface configuration - ntp: + sonic_ntp: config: source_interfaces: - Ethernet8 @@ -205,7 +215,7 @@ EXAMPLES = """ #ntp authentication-key 20 sha2-256 U2FsdGVkX1/eAzKj1teKhYWD7tnzOsYOijGeFAT0rKM= encrypted # - name: Delete NTP key configuration - ntp: + sonic_ntp: config: ntp_keys: - key_id: 10 @@ -225,14 +235,14 @@ EXAMPLES = """ # ------------- # #sonic# show ntp server -#---------------------------------------------------------------------- -#NTP Servers minpoll maxpoll Authentication key ID -#---------------------------------------------------------------------- -#10.11.0.1 6 10 -#dell.com 6 9 +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.1 6 10 False +#dell.com 6 9 False # - name: Merge NTP server configuration - ntp: + sonic_ntp: config: servers: - address: 10.11.0.2 @@ -240,19 +250,20 @@ EXAMPLES = """ - address: dell.org minpoll: 7 maxpoll: 10 + prefer: true 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 +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.1 6 10 Flase +#10.11.0.2 5 10 Flase +#dell.com 6 9 Flase +#dell.org 7 10 True # # # Using merged @@ -267,7 +278,7 @@ EXAMPLES = """ #NTP source-interfaces: Ethernet0, Ethernet4 # - name: Merge NTP source-interface configuration - ntp: + sonic_ntp: config: source_interfaces: - Ethernet8 @@ -293,7 +304,7 @@ EXAMPLES = """ #ntp authentication-key 8 sha1 U2FsdGVkX1/NpJrdOeyMeUHEkSohY6azY9VwbAqXRTY= encrypted # - name: Merge NTP key configuration - ntp: + sonic_ntp: config: ntp_keys: - key_id: 10 @@ -314,6 +325,87 @@ EXAMPLES = """ #ntp authentication-key 10 md5 U2FsdGVkX1/Gxds/5pscCvIKbVngGaKka4SQineS51Y= encrypted #ntp authentication-key 20 sha2-256 U2FsdGVkX1/eAzKj1teKhYWD7tnzOsYOijGeFAT0rKM= encrypted # +# Using replaced +# +# Before state: +# ------------- +# +#sonic# show ntp server +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.1 6 10 False +#dell.com 6 9 False +# +- name: Replace NTP server configuration + sonic_ntp: + config: + servers: + - address: 10.11.0.2 + minpoll: 5 + maxpoll: 9 + - address: dell.com + minpoll: 7 + maxpoll: 10 + prefer: true + state: replaced +# +# After state: +# ------------ +# +#sonic# show ntp server +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.1 6 10 False +#10.11.0.2 5 9 False +#dell.com 7 10 True +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic# show ntp server +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.1 6 10 False +#dell.com 6 9 False +# +#sonic# show ntp global +#---------------------------------------------- +#NTP Global Configuration +#---------------------------------------------- +#NTP source-interfaces: Ethernet0, Ethernet4 +# +- name: Overridden NTP configuration + sonic_ntp: + config: + servers: + - address: 10.11.0.2 + minpoll: 5 + - address: dell.com + minpoll: 7 + maxpoll: 10 + prefer: true + state: overridden +# +# After state: +# ------------ +# +# After state: +# ------------ +# +#sonic# show ntp server +#---------------------------------------------------------------------------- +#NTP Servers minpoll maxpoll Prefer Authentication key ID +#---------------------------------------------------------------------------- +#10.11.0.2 5 10 False +#dell.com 7 10 True +# +#sonic# show ntp global +# """ RETURN = """ before: @@ -330,6 +422,13 @@ after: sample: > The configuration returned will always be in the same format of the parameters above. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_pki.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_pki.py new file mode 100644 index 000000000..559935fef --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_pki.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2022 Dell EMC +# 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_pki +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_pki +version_added: 2.3.0 +short_description: 'Manages PKI attributes of Enterprise Sonic' +description: 'Manages PKI attributes of Enterprise Sonic' +author: Eric Seifert (@seiferteric) +notes: + - 'Tested against Dell Enterprise SONiC 4.1.0' +options: + config: + description: The provided configuration + type: dict + suboptions: + trust_stores: + description: Store of CA Certificates + type: list + elements: dict + suboptions: + name: + type: str + required: True + description: The name of the Trust Store + ca_name: + type: list + elements: str + description: List of CA certificates in the trust store. + security_profiles: + description: Application Security Profiles + type: list + elements: dict + suboptions: + profile_name: + type: str + required: True + description: Profile Name + certificate_name: + type: str + description: Host Certificate Name + trust_store: + type: str + description: Name of associated trust_store + revocation_check: + description: Require certificate revocation check succeeds + type: bool + peer_name_check: + description: Require peer name is verified + type: bool + key_usage_check: + description: Require key usage is enforced + type: bool + cdp_list: + description: Global list of CDP's + type: list + elements: str + ocsp_responder_list: + description: Global list of OCSP responders + type: list + elements: str + state: + description: + - The state of the configuration after module completion. + type: str + choices: ['merged', 'deleted', 'replaced', 'overridden'] + default: merged +""" +EXAMPLES = """ +# Using "merged" state for initial config +# +# Before state: +# ------------- +# +# sonic# show running-configuration | grep crypto +# sonic# +# +- name: PKI Config Test + hosts: datacenter + gather_facts: false + connection: httpapi + collections: + - dellemc.enterprise_sonic + tasks: + - name: "Initial Config" + sonic_pki: + config: + security_profiles: + - profile_name: rest + ocsp_responder_list: + - http://example.com/ocspa + - http://example.com/ocspb + certificate_name: host + trust_store: default-ts + trust_stores: + - name: default-ts + ca_name: + - CA2 + state: merged + +# After state: +# ------------ +# +# sonic# show running-configuration | grep crypto +# crypto trust_store default-ts ca-cert CA2 +# crypto security-profile rest +# crypto security-profile trust_store rest default-ts +# crypto security-profile certificate rest host +# crypto security-profile ocsp-list rest http://example.com/ocspa,http://example.com/ocspb + +# Using "deleted" state to remove configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration | grep crypto +# crypto trust_store default-ts ca-cert CA2 +# crypto security-profile rest +# crypto security-profile trust_store rest default-ts +# crypto security-profile certificate rest host +# crypto security-profile ocsp-list rest http://example.com/ocsp +# +- name: PKI Delete Test + hosts: datacenter + gather_facts: true + connection: httpapi + collections: + - dellemc.enterprise_sonic + tasks: + - name: Remove trust_store from security-profile + sonic_pki: + config: + security_profiles: + - profile_name: rest + trust_store: default-ts + state: deleted +# After state: +# ------------ +# +# sonic# show running-configuration | grep crypto +# crypto trust_store default-ts ca-cert CA2 +# crypto security-profile rest +# crypto security-profile certificate rest host +# crypto security-profile ocsp-list rest http://example.com/ocsp + +# Using "overridden" state + +# Before state: +# ------------ +# +# sonic# show running-configuration | grep crypto +# crypto trust_store default-ts ca-cert CA2 +# crypto security-profile rest +# crypto security-profile trust_store rest default-ts +# crypto security-profile certificate rest host +# crypto security-profile ocsp-list rest http://example.com/ocspa,http://example.com/ocspb +# +- name: PKI Overridden Test + hosts: datacenter + gather_facts: false + connection: httpapi + collections: + - dellemc.enterprise_sonic + tasks: + - name: "Overridden Config" + sonic_pki: + config: + security_profiles: + - profile_name: telemetry + ocsp_responder_list: + - http://example.com/ocspb + revocation_check: true + trust_store: telemetry-ts + certificate_name: host + trust_stores: + - name: telemetry-ts + ca_name: CA + state: overridden +# After state: +# ----------- +# +# sonic# show running-configuration | grep crypto +# crypto trust_store telemetry-ts ca-cert CA +# crypto security-profile telemetry revocation_check true +# crypto security-profile trust_store telemetry telemetry-ts +# crypto security-profile certificate telemetry host +# crypto security-profile ocsp-list telemetry http://example.com/ocspb + +# Using "replaced" state to update config + +# Before state: +# ------------ +# +# sonic# show running-configuration | grep crypto +# crypto trust_store default-ts ca-cert CA2 +# crypto security-profile rest +# crypto security-profile trust_store rest default-ts +# crypto security-profile certificate rest host +# crypto security-profile ocsp-list rest http://example.com/ocspa,http://example.com/ocspb +# +- name: PKI Replace Test + hosts: datacenter + gather_facts: false + connection: httpapi + collections: + - dellemc.enterprise_sonic + tasks: + - name: "Replace Config" + sonic_pki: + config: + security_profiles: + - profile_name: rest + ocsp_responder_list: + - http://example.com/ocsp + revocation_check: false + trust_store: default-ts + certificate_name: host + state: replaced +# After state: +# ----------- +# +# sonic# show running-configuration | grep crypto +# crypto trust_store default-ts ca-cert CA2 +# crypto security-profile rest +# crypto security-profile trust_store rest default-ts +# crypto security-profile certificate rest host +# crypto security-profile ocsp-list rest http://example.com/ocsp + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + 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.pki.pki import PkiArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.pki.pki import Pki + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=PkiArgs.argument_spec, + supports_check_mode=True) + + result = Pki(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 index 66ea00476..3de7dfb17 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_breakout.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_breakout.py @@ -57,23 +57,35 @@ options: - Specifies the mode of the port breakout. type: str choices: + - 1x10G + - 1x25G + - 1x40G + - 1x50G - 1x100G + - 1x200G - 1x400G - - 1x40G + - 2x10G + - 2x25G + - 2x40G + - 2x50G - 2x100G - 2x200G - - 2x50G - - 4x100G - 4x10G - 4x25G - 4x50G + - 4x100G + - 8x10G + - 8x25G + - 8x50G 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. + - In case of deleted, the existing port breakout mode configuration will be removed from the device. + - In case of replaced, on-device port breakout configuration of the specified interfaces is replaced with provided configuration. + - In case of overridden, all on-device port breakout configurations are overridden with the provided configuration. default: merged - choices: ['merged', 'deleted'] + choices: ['merged', 'deleted', 'replaced', 'overridden'] type: str """ EXAMPLES = """ @@ -82,18 +94,18 @@ EXAMPLES = """ # 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 +# sonic# 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/1 # -- name: Merge users configurations +- name: Delete interface port breakout configuration dellemc.enterprise_sonic.sonic_port_breakout: config: - name: 1/11 @@ -103,15 +115,16 @@ EXAMPLES = """ # 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 +# sonic# 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 Eth1/11 +# # Using deleted @@ -119,31 +132,31 @@ EXAMPLES = """ # 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 +# sonic# 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/1 +# + +- name: Delete all port breakout 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 +# sonic# show interface breakout +# ----------------------------------------------- +# Port Breakout Mode Status Interfaces +# ----------------------------------------------- +# 1/1 Default Completed Eth1/1 +# 1/11 Default Completed Eth1/11 # Using merged @@ -151,35 +164,111 @@ EXAMPLES = """ # 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 +# sonic# 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 + +- name: Merge port breakout configurations dellemc.enterprise_sonic.sonic_port_breakout: config: - name: 1/11 mode: 1x100G state: merged +# After state: +# ------------ +# +# sonic# 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/1 + + +# Using replaced +# +# Before state: +# ------------- +# +# sonic# show interface breakout +# ----------------------------------------------- +# Port Breakout Mode Status Interfaces +# ----------------------------------------------- +# 1/49 4x25G Completed Eth1/49/1 +# Eth1/49/2 +# Eth1/49/3 +# Eth1/49/4 +# + +- name: Replace port breakout configurations + dellemc.enterprise_sonic.sonic_port_breakout: + config: + - name: 1/49 + mode: 4x10G + state: replaced + +# After state: +# ------------ +# +# sonic# show interface breakout +# ----------------------------------------------- +# Port Breakout Mode Status Interfaces +# ----------------------------------------------- +# 1/49 4x10G Completed Eth1/49/1 +# Eth1/49/2 +# Eth1/49/3 +# Eth1/49/4 + + +# Using overridden +# +# Before state: +# ------------- +# +# sonic# show interface breakout +# ---------------------------------------------- +# Port Breakout Mode Status Interfaces +# ----------------------------------------------- +# 1/49 4x10G Completed Eth1/49/1 +# Eth1/49/2 +# Eth1/49/3 +# Eth1/49/4 +# 1/50 2x50G Completed Eth1/50/1 +# Eth1/50/2 +# 1/51 1x100G Completed Eth1/51/1 +# + +- name: Override port breakout configurations + dellemc.enterprise_sonic.sonic_port_breakout: + config: + - name: 1/52 + mode: 4x10G + state: overridden # 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 +# sonic# show interface breakout +# ----------------------------------------------- +# Port Breakout Mode Status Interfaces +# ----------------------------------------------- +# 1/49 Default Completed Eth1/49 +# 1/50 Default Completed Eth1/50 +# 1/51 Default Completed Eth1/51 +# 1/52 4x10G Completed Eth1/52/1 +# Eth1/52/2 +# Eth1/52/3 +# Eth1/52/4 """ diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_group.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_group.py new file mode 100644 index 000000000..d31c19cd3 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_port_group.py @@ -0,0 +1,370 @@ +#!/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_port_group +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_port_group +version_added: 2.1.0 +notes: + - Supports C(check_mode). +short_description: Manages port group configuration on SONiC. +description: + - This module provides configuration management of port group for devices running SONiC. +author: 'M. Zhang (@mingjunzhang2019)' +options: + config: + description: + - A list of port group configurations. + type: list + elements: dict + suboptions: + id: + type: str + description: + - The index of the port group. + required: true + speed: + description: + - Speed for the port group. + - This configures the speed for all the memebr ports of the prot group. + - Supported speeds are dependent on the type of switch. + type: str + choices: + - SPEED_10MB + - SPEED_100MB + - SPEED_1GB + - SPEED_2500MB + - SPEED_5GB + - SPEED_10GB + - SPEED_20GB + - SPEED_25GB + - SPEED_40GB + - SPEED_50GB + - SPEED_100GB + - SPEED_400GB + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 10G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 10G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +- name: Configure port group speed + sonic_port_group: + config: + - id: 1 + - id: 10 + state: deleted +# +# +# After state: +# ------------ +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 25G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 10G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 10G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 10G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +- name: Configure port group speed + sonic_port_group: + config: + - id: + state: deleted +# +# +# After state: +# ------------ +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 25G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 25G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +# Using merged +# +# Before state: +# ------------- +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 25G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 25G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +- name: Configure port group speed + sonic_port_group: + config: + - id: 1 + speed: SPEED_10GB + - id: 9 + speed: SPEED_10GB + state: merged +# +# +# After state: +# ------------ +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 10G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 10G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +# Using replaced +# +# Before state: +# ------------- +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 25G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 10G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 25G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +- name: Replace port group speed + sonic_port_group: + config: + - id: 1 + speed: SPEED_10GB + - id: 9 + speed: SPEED_10GB + state: replaced +# +# After state: +# ------------ +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 10G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 10G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 10G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 25G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 10G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 10G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 10G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 10G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 10G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 10G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 10G +# +- name: Override port group speed + sonic_port_group: + config: + - id: 1 + speed: SPEED_10GB + - id: 9 + speed: SPEED_10GB + state: overridden +# +# After state: +# ------------ +# +#sonic# show port-group +#------------------------------------------------------------------------------------- +#Port-group Interface range Valid speeds Default Speed Current Speed +#------------------------------------------------------------------------------------- +#1 Ethernet0 - Ethernet3 10G, 25G 25G 10G +#2 Ethernet4 - Ethernet7 10G, 25G 25G 25G +#3 Ethernet8 - Ethernet11 10G, 25G 25G 25G +#4 Ethernet12 - Ethernet15 10G, 25G 25G 25G +#5 Ethernet16 - Ethernet19 10G, 25G 25G 25G +#6 Ethernet20 - Ethernet23 10G, 25G 25G 25G +#7 Ethernet24 - Ethernet27 10G, 25G 25G 25G +#8 Ethernet28 - Ethernet31 10G, 25G 25G 25G +#9 Ethernet32 - Ethernet35 10G, 25G 25G 10G +#10 Ethernet36 - Ethernet39 10G, 25G 25G 25G +# +""" +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. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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_group.port_group import Port_groupArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.port_group.port_group import Port_group + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Port_groupArgs.argument_spec, + supports_check_mode=True) + + result = Port_group(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 index 5a734e8b2..b3389b6ad 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_prefix_lists.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_prefix_lists.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2019 Red Hat +# Copyright 2023 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) @@ -87,11 +87,15 @@ options: 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. + - For "deleted", delete the specified attributes from existing configuration. + - For "replaced", replace the specified existing configuration with the provided configuration. + - For "overridden", override the existing configuration with the provided configuration. type: str choices: - merged - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -227,6 +231,95 @@ EXAMPLES = """ # sonic# # (no IPv6 prefix-list configuration present) # +# *************************************************************** +# Using "overriden" state to override 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 pfx3 seq 20 deny 1.2.3.12/26 +# ip prefix-list pfx4 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: Override prefix-list configuration + dellemc.enterprise_sonic.sonic_prefix_lists: + config: + - name: pfx2 + afi: "ipv4" + prefixes: + - sequence: 10 + prefix: "10.20.30.128/24" + action: "deny" + ge: 25 + le: 30 + state: overridden + +# After state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx2 seq 10 deny 10.20.30.128/24 ge 25 le 30 +# +# sonic# show running-configuration ipv6 prefix-list +# sonic# +# (no IPv6 prefix-list configuration present) +# +# *************************************************************** +# Using "replaced" state to replace configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx2 seq 10 deny 10.20.30.128/24 ge 25 le 30 +# +# sonic# show running-configuration ipv6 prefix-list +# sonic# +# (no IPv6 prefix-list configuration present) +# +# ------------ +# +- name: Replace prefix-list configuration + dellemc.enterprise_sonic.sonic_prefix_lists: + config: + - name: pfx2 + afi: "ipv4" + prefixes: + - sequence: 10 + prefix: "10.20.30.128/24" + action: "permit" + ge: 25 + le: 30 + - name: pfx3 + afi: "ipv6" + prefixes: + - sequence: 20 + action: "deny" + prefix: "60::70/124" + state: replaced + +# After state: +# ------------ +# +# sonic# show running-configuration ip prefix-list +# ! +# ip prefix-list pfx2 seq 10 permit 10.20.30.128/24 ge 25 le 30 +# +# sonic# show running-configuration ipv6 prefix-list +# sonic# +# ! +# ipv6 prefix-list pfx3 seq 20 deny 60::70/124 +# """ RETURN = """ before: 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 index 1df4aff61..bc1f81d39 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_radius_server.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_radius_server.py @@ -71,6 +71,7 @@ options: description: - Specifies the timeout of the radius server. type: int + default: 5 retransmit: description: - Specifies the re-transmit value of the radius server. @@ -110,6 +111,7 @@ options: description: - Specifies the port of the radius server host. type: int + default: 1812 timeout: description: - Specifies the timeout of the radius server host. @@ -131,8 +133,10 @@ options: - 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. + - In case of replaced, the existing radius server configuration will be replaced with provided configuration. + - In case of overridden, the existing radius server configuration will be overridden with the provided configuration. default: merged - choices: ['merged', 'deleted'] + choices: ['merged', 'replaced', 'overridden', 'deleted'] type: str """ EXAMPLES = """ @@ -280,8 +284,106 @@ EXAMPLES = """ #--------------------------------------------------------- #RADIUS Statistics #--------------------------------------------------------- - - +# +# Using replaced +# +# Before state: +# ------------- +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#timeout : 10 +#auth-type : pap +#key configured : Yes +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------------- +#1.2.3.4 pap No 49 1 5 - - Ethernet0 +# +- name: Replace radius configurations + sonic_radius_server: + config: + auth_type: mschapv2 + timeout: 20 + servers: + - host: + name: 1.2.3.4 + auth_type: mschapv2 + key: mschapv2 + source_interface: Ethernet12 + state: replaced +# +# After state: +# ------------ +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#timeout : 20 +#auth-type : mschapv2 +#key configured : No +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------------- +#1.2.3.4 mschapv2 Yes 1812 - - - - Ethernet12 +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#timeout : 10 +#auth-type : pap +#key configured : Yes +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------------- +#1.2.3.4 pap No 49 1 5 - - Ethernet0 +#11.12.13.14 chap Yes 49 10 5 3 - - +# +- name: Override radius configurations + sonic_radius_server: + config: + auth_type: mschapv2 + key: mschapv2 + timeout: 20 + servers: + - host: + name: 1.2.3.4 + auth_type: mschapv2 + key: mschapv2 + source_interface: Ethernet12 + - host: + name: 10.10.11.12 + auth_type: chap + timeout: 30 + priority: 2 + port: 49 + state: overridden +# +# After state: +# ------------ +# +#sonic(config)# do show radius-server +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +#timeout : 20 +#auth-type : mschapv2 +#key configured : Yes +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG AUTH-PORT PRIORITY TIMEOUT RTSMT VRF SI +#-------------------------------------------------------------------------------------- +#1.2.3.4 mschapv2 Yes 1812 - - - - Ethernet12 +#10.10.11.12 chap No 49 2 30 - - - +# """ RETURN = """ before: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_route_maps.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_route_maps.py new file mode 100644 index 000000000..01327572e --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_route_maps.py @@ -0,0 +1,1606 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 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_route_maps +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_route_maps +version_added: "2.1.0" +author: "Kerry Meyer (@kerry-meyer)" +short_description: route map configuration handling for SONiC +description: + - This module provides configuration management for route map parameters on devices running SONiC. +options: + config: + description: + - Specifies a list of route map configuration dictionaries + type: list + elements: dict + suboptions: + map_name: + description: + - Name of a route map + type: str + required: true + action: + description: + - action type for the route map (permit or deny) + - This value is required for creation and modification of a route + - map or route map attributes as well as for deletion of route map + - attributes. It can be omitted only when requesting deletion of a + - route map statement or all route map statements for a given route + - map map_name. + type: str + choices: + - permit + - deny + sequence_num: + description: + - unique number in the range 1-66535 to specify priority of the map + - This value is required for creation and modification of a route + - map or route map attributes as well as for deletion of route map + - attributes. It can be omitted only when requesting deletion of all + - route map "statements" for a given route map "map_name". + type: int + match: + description: Criteria for matching the route map to a route + type: dict + suboptions: + as_path: + description: + - Name of a configured BGP AS path list to be checked for + - a match with the target route + type: str + community: + description: + - Name of a configured BGP "community" to be checked for + - a match with the target route + type: str + evpn: + description: + - BGP Ethernet Virtual Private Network to be checked for + - a match with the target route + type: dict + suboptions: + default_route: + description: + - Default EVPN type-5 route + type: bool + route_type: + description: + - "Non-default route type: One of the following:" + - mac-ip route, EVPN Type 3 Inclusive Multicast Ethernet + - Tag (IMET) route, or prefix route + type: str + choices: + - macip + - multicast + - prefix + vni: + description: + - VNI ID to be checked for a match; specified by a value in the + - range 1-16777215 + type: int + ext_comm: + description: + - Name of a configured BGP 'extended community' to be checked for + - a match with the target route + type: str + interface: + description: + - Next hop interface name (type and number) to be checked for a + - match with the target route. The interface type can be any + - of the following; 'Ethernet/Eth' interface or sub-interface, + - "'Loopback' interface, 'PortChannel' interface or" + - "sub-interface, 'Vlan' interface." + type: str + ip: + description: + - IP addresses or IP next hops to be checked for a match with the + - target route + type: dict + suboptions: + address: + description: + - name of an IPv4 prefix list containing a list of address + - prefixes to be checked for a match with the target route + type: str + next_hop: + description: + - name of a prefix list containing a list of next-hop + - prefixes to be checked for a match with the target route + type: str + ipv6: + description: + - IPv6 addresses to be checked for a match with the + - target route + type: dict + suboptions: + address: + description: + - name of an IPv6 prefix list containing a list of address + - prefixes to be checked for a match with the target route + type: str + required: true + local_preference: + description: + - local-preference value to be checked for a match with the + - target route. This is a value in the range 0-4294967295. + type: int + metric: + description: + - metric value to be checked for a match with the target route. + - This is a value in the range 0-4294967295. + type: int + origin: + description: + - BGP origin to be checked for a match with the target route + type: str + choices: + - egp + - igp + - incomplete + peer: + description: + - BGP routing peer/neighbor required for a matching route. + - I(ip), I(ipv6), and I(interface) are mutually exclusive. + type: dict + suboptions: + ip: + description: IPv4 address of a BGP peer + type: str + ipv6: + description: IPv6 address of a BGP peer + type: str + interface: + description: + - Name (type and number) of a BGP peer interface. + - Allowed interface types are Ethernet or Eth (depending + - on the configured interface-naming mode), + - Vlan, and Portchannel + type: str + source_protocol: + description: Source protocol required for a matching route + type: str + choices: + - bgp + - connected + - ospf + - static + source_vrf: + description: Name of the source VRF required for a matching route + type: str + tag: + description: + - Tag value required for a matching route + - The value must be in the range 1-4294967295 + type: int + set: + description: Information to set into a matching route for re-distribution + type: dict + suboptions: + as_path_prepend: + description: + - String specifying a comma-separated list of AS-path numbers + - "to prepend to the BGP AS-path attribute in a matched route." + - AS-path values in the list must be in the range + - "1-4294967295; for example, 2000,3000" + type: str + comm_list_delete: + description: + - String specifying the name of a BGP community list containing + - BGP Community values to be deleted from matching routes. + type: str + community: + description: + - BGP community attributes to add to or replace the BGP + - community attributes in a matching route. Specifying the + - "'additive' attribute is allowed only if one of" + - the other attributes (other than 'none') is specified. + - It causes the specified 'set community' attributes + - to be added to the already existing community + - "attributes in the matching route. If the 'additive' attribute" + - is not specified, the previously existing community attributes + - in the matching route are replaced by the configured + - "'set community' attributes. Specifying a 'set community' attribute" + - of 'none' is mutually exclusive with setting of other community + - attributes and causes any community attributes in the matching + - route to be removed. + type: dict + suboptions: + community_number: + description: + - A list of one or more BGP community numbers in the + - "form AA:NN where AA and NN are integers in the range" + - "0-65535." + - "Note: Each community number in the list must be enclosed" + - in double quotes to avoid YAML parsing errors due to the + - "list values containing an embedded ':' character." + type: list + elements: str + community_attributes: + description: + - A list of one or more BGP community attributes. The allowed + - "values are the following:" + - local_as + - Do not send outside local AS (well-known community) + - no_advertise + - Do not advertise to any peer (well-known community) + - no_export + - Do not export to next AS (well-known community) + - no_peer + - "The route does not need to be advertised to peers." + - (Advertisement of the route can be suppressed based + - on other criteria.) + - additive + - Add the configured 'set community' attributes to + - "the matching route (if set to 'true'); Previously existing" + - attributes in the matching route are, instead, replaced + - by the configured attributes if this attribute is + - not specified or if it is set to 'false'. + - none + - Do not send any community attribute. This attribute + - is mutually exclusive with all other 'set community' + - attributes. It causes all attributes to be removed + - from the matching route. + - "I(none) is mutually exclusive with all of the other attributes:" + - I(local_as), I(no_advertise), I(no_export), I(no_peer), I(additive), + - and I(additive). + type: list + elements: str + choices: + - local_as + - no_advertise + - no_export + - no_peer + - additive + - none + extcommunity: + description: + - BGP extended community attributes to set into a matching route. + type: dict + suboptions: + rt: + description: + - Route Target VPN extended communities in the format + - "ASN:NN or IP-ADDRESS:NN" + - "Note: Each rt value in the list must be enclosed" + - in double quotes to avoid YAML parsing errors due to the + - "list values containing an embedded ':' character." + type: list + elements: str + soo: + description: + - "Site-of-Origin VPN extended communities in the format" + - "ASN:NN or IP-ADDRESS:NN" + - "Note: Each rt value in the list must be enclosed" + - in double quotes to avoid YAML parsing errors due to the + - "list values containing an embedded ':' character." + type: list + elements: str + ip_next_hop: + description: + - IPv4 next hop address to set into a matching route in the + - dotted decimal format A.B.C.D + type: str + ipv6_next_hop: + description: + - IPv6 next hop address attributes to set into a matching route + type: dict + suboptions: + global_addr: + description: + - IPv6 global next hop address to set into a matching + - "route in the format A::B" + type: str + prefer_global: + description: + - Set the corresponding attribute into a matching route + - if the value of this Ansible attribute is 'true'. + - The attribute indicates that the routing algorithm must + - prefer the global next-hop address over the link-local + - address if both exist. + type: bool + local_preference: + description: + - "BGP local preference path attribute; integer value in" + - the range 0-4294967295 + type: int + metric: + description: + - route metric value actions + - I(value) and I(rtt_action) are mutually exclusive. + type: dict + suboptions: + value: + description: + - "metric value to be set into a matching route;" + - value in the range 0-4294967295 + type: int + rtt_action: + description: + - Action to take for modifying the metric for a matched + - "route using the Round Trip Time (rtt);" + - C(set) causes the route metric to be set to the + - rtt value. + - C(add) causes the rtt value to be added + - to the route metric. + - C(subtract) causes the rtt value to be + - subtracted from route metric. + type: str + choices: + - set + - add + - subtract + origin: + description: + - "BGP route origin; One of the following must be selected." + - "egp (External; remote EGP)" + - "igp (Internal; local IGP)" + - incomplete (Unknown origin) + type: str + choices: + - egp + - igp + - incomplete + weight: + description: + - BGP weight for the routing table. The weight must be an + - integer in the range 0-4294967295 + type: int + call: + description: + - Name of a route map to jump to after executing 'match' and 'set' + - statements for the current route map. + type: str + + state: + description: + - Specifies the type of configuration update to be performed on the device. + - For C(merged), merge specified attributes with existing configured attributes. + - For C(deleted), delete the specified attributes from existing configuration. + - For C(replaced), replace each modified list or dictionary with the + - specified items. + - For C(overridden), replace all current configuration for this resource + - module with the specified configuration. + type: str + choices: + - merged + - deleted + - replaced + - overridden + default: merged +""" +EXAMPLES = """ +# Using "merged" state to create initial configuration +# +# Before state: +# ------------- +# +# sonic# show running-configuration route-map +# sonic# +# (No configuration present) +# +# ------------- +# +- name: Merge initial route_maps configuration + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm1 + action: permit + sequence_num: 80 + match: + as_path: bgp_as1 + community: bgp_comm_list1 + evpn: + default_route: true + vni: 735 + ext_comm: bgp_ext_comm1 + interface: Ethernet4 + ip: + address: ip_pfx_list1 + ipv6: + address: ipv6_pfx_list1 + local_preference: 8000 + metric: 400 + origin: egp + peer: + ip: 10.20.30.40 + source_protocol: bgp + source_vrf: Vrf1 + tag: 7284 + set: + as_path_prepend: 200,315,7135 + comm_list_delete: bgp_comm_list2 + community: + community_number: + - "35:58" + - "79:150" + - "308:650" + community_attributes: + - local_as + - no_advertise + - no_export + - no_peer + - additive + extcommunity: + rt: + - "30:40" + soo: + - "10.73.14.9:78" + ip_next_hop: 10.48.16.18 + ipv6_next_hop: + global_addr: 30::30 + prefer_global: true + local_preference: 635 + metric: + metric_value: 870 + origin: egp + weight: 93471 + - map_name: rm1 + action: deny + sequence_num: 3047 + match: + evpn: + route_type: multicast + origin: incomplete + peer: + interface: Ethernet6 + source_protocol: ospf + set: + metric: + rtt_action: add + origin: incomplete + - map_name: rm3 + action: deny + sequence_num: 285 + match: + evpn: + route_type: macip + origin: igp + peer: + ipv6: 87:95:15::53 + source_protocol: connected + set: + community: + community_attributes: + - none + metric: + rtt_action: set + origin: igp + call: rm1 + - map_name: rm4 + action: permit + sequence_num: 480 + match: + evpn: + route_type: prefix + source_protocol: static + set: + metric: + rtt_action: subtract + state: merged + +# After state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match as-path bgp_as1 +# match evpn default-route +# match evpn vni 735 +# match ip address prefix-list ip_pfx_list1 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Ethernet4 +# match community bgp_comm_list1 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match peer 10.20.30.40 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 200,315,7135 +# set community 35:58 79:150 308:650 local-AS no-advertise no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric 870 +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set ipv6 next-hop prefer-global +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match evpn route-type multicast +# match peer Ethernet6 +# match source-protocol ospf +# match origin incomplete +# set metric +rtt +# set origin incomplete +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# call rm1 +# match peer 87:95:15::53 +# match source-protocol connected +# match origin igp +# set community none +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt +# ------------ + + +# Using "merged" state to update and add configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match as-path bgp_as1 +# match evpn default-route +# match evpn vni 735 +# match ip address prefix-list ip_pfx_list1 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Ethernet4 +# match community bgp_comm_list1 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match peer 10.20.30.40 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 200,315,7135 +# set community 35:58 79:150 308:650 local-AS no-advertise no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric 870 +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set ipv6 next-hop prefer-global +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match evpn route-type multicast +# match peer Ethernet6 +# match source-protocol ospf +# match origin incomplete +# set metric +rtt +# set origin incomplete +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# call rm1 +# match peer 87:95:15::53 +# match source-protocol connected +# match origin igp +# set community none +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt +# ------------ +# +- name: Merge additional and modified route map configuration + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm1 + action: permit + sequence_num: 80 + match: + as_path: bgp_as2 + community: bgp_comm_list3 + evpn: + route_type: prefix + vni: 850 + interface: Vlan7 + ip: + address: ip_pfx_list2 + next_hop: ip_pfx_list3 + peer: + interface: Portchannel14 + set: + as_path_prepend: 188,257 + community: + community_number: + - "45:736" + ipv6_next_hop: + prefer_global: false + metric: + rtt_action: add + - map_name: rm1 + action: deny + sequence_num: 3047 + match: + as_path: bgp_as3 + ext_comm: bgp_ext_comm2 + origin: igp + set: + metric: + rtt_action: subtract + - map_name: rm2 + action: permit + sequence_num: 100 + match: + interface: Ethernet16 + set: + as_path_prepend: 200,300,400 + ipv6_next_hop: + global_addr: 37::58 + prefer_global: true + metric: 8000 + - map_name: rm3 + action: deny + sequence_num: 285 + match: + local_preference: 14783 + source_protocol: bgp + set: + community: + community_attributes: + - no_advertise + state: merged + +# After state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn default-route +# match evpn route-type prefix +# match evpn vni 850 +# match ip address prefix-list ip_pfx_list2 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community 35:58 79:150 308:650 45:736 local-AS no-advertise no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match as-path bgp_as3 +# match evpn route-type multicast +# match ext-community bgp_ext_comm2 +# match peer Ethernet6 +# match source-protocol ospf +# match origin igp +# set metric -rtt +# set origin incomplete +# ! +# route-map rm2 permit 100 +# match interface Ethernet16 +# set as-path prepend 200,300,400 +# set ipv6 next-hop global 37::58 +# set ipv6 next-hop prefer-global +# set metric 8000 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt + + +# Using "replaced" state to replace the contents of a list +# +# Before state: +# ------------ +# +# sonic(config-route-map)# do show running-configuration route-map rm1 80 +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn default-route +# match evpn route-type prefix +# match evpn vni 850 +# match ip address prefix-list ip_pfx_list2 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community 35:58 79:150 308:650 45:736 local-AS no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ------------ +- name: Replace a list + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm1 + action: permit + sequence_num: 80 + set: + community: + community_number: + - "15:30" + - "26:54" + state: replaced + +# After state: +# ------------ +# +# sonic#show running-configuration route-map rm1 80 +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn default-route +# match evpn route-type prefix +# match evpn vni 850 +# match ip address prefix-list ip_pfx_list2 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community 15:30 26:54 local-AS no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 + + +# Using "replaced" state to replace the contents of dictionaries +# +# Before state: +# ------------ +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn default-route +# match evpn route-type prefix +# match evpn vni 850 +# match ip address prefix-list ip_pfx_list2 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community 15:30 26:54 local-AS no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match as-path bgp_as3 +# match evpn route-type multicast +# match ext-community bgp_ext_comm2 +# match peer Ethernet6 +# match source-protocol ospf +# match origin igp +# set metric -rtt +# set origin incomplete +# ! +# route-map rm2 permit 100 +# match interface Ethernet16 +# set as-path prepend 200,300,400 +# set ipv6 next-hop global 37::58 +# set ipv6 next-hop prefer-global +# set metric 8000 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt +# ------------ +- name: Replace dictionaries + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm1 + action: permit + sequence_num: 80 + match: + evpn: + route_type: multicast + ip: + address: ip_pfx_list1 + set: + community: + community_attributes: + - no_advertise + extcommunity: + rt: + - "20:20" + + - map_name: rm2 + action: permit + sequence_num: 100 + set: + ipv6_next_hop: + global_addr: 45::90 + state: replaced + +# After state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn route-type multicast +# match ip address prefix-list ip_pfx_list1 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community no-advertise +# set extcommunity rt 20:20 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match as-path bgp_as3 +# match evpn route-type multicast +# match ext-community bgp_ext_comm2 +# match peer Ethernet6 +# match source-protocol ospf +# match origin igp +# set metric -rtt +# set origin incomplete +# ! +# route-map rm2 permit 100 +# match interface Ethernet16 +# set as-path prepend 200,300,400 +# set metric 8000 +# set ipv6 next-hop global 45::90 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt + + +# Using "overridden" state to override all existing configuration with new +# configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn route-type multicast +# match ip address prefix-list ip_pfx_list1 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community no-advertise +# set extcommunity rt 30:40 +# set extcommunity rt 20:20 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match as-path bgp_as3 +# match evpn route-type multicast +# match ext-community bgp_ext_comm2 +# match peer Ethernet6 +# match source-protocol ospf +# match origin igp +# set metric -rtt +# set origin incomplete +# ! +# route-map rm2 permit 100 +# match interface Ethernet16 +# set as-path prepend 200,300,400 +# set metric 8000 +# set ipv6 next-hop global 45::90 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt +# ------------ +- name: Override all route map configuration with new configuration + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm5 + action: permit + sequence_num: 250 + match: + interface: Ethernet28 + set: + as_path_prepend: 150,275 + metric: 7249 + state: overridden + +# After state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm5 permit 250 +# match interface Ethernet28 +# set as-path prepend 150,275 +# set metric 7249 + + +# Using "overridden" state to override all existing configuration with new +# configuration. (Restore previous configuration.) +# +# Before state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm5 permit 250 +# match interface Ethernet28 +# set as-path prepend 150,275 +# set metric 7249 +# ------------ +- name: Override (restore) all route map configuration with older configuration + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm1 + action: permit + sequence_num: 80 + match: + as_path: bgp_as2 + community: bgp_comm_list3 + evpn: + default_route: true + route_type: prefix + vni: 850 + ext_comm: bgp_ext_comm1 + interface: Vlan7 + ip: + address: ip_pfx_list2 + next_hop: ip_pfx_list3 + ipv6: + address: ipv6_pfx_list1 + local_preference: 8000 + metric: 400 + origin: egp + peer: + interface: Portchannel14 + source_protocol: bgp + source_vrf: Vrf1 + tag: 7284 + set: + as_path_prepend: 188,257 + comm_list_delete: bgp_comm_list2 + community: + community_number: + - "35:58" + - "79:150" + - "308:650" + - "45:736" + community_attributes: + - local_as + - no_export + - no_peer + - additive + extcommunity: + rt: + - "30:40" + soo: + - "10.73.14.9:78" + ip_next_hop: 10.48.16.18 + ipv6_next_hop: + global_addr: 30::30 + local_preference: 635 + metric: + rtt_action: add + origin: egp + weight: 93471 + - map_name: rm1 + action: deny + sequence_num: 3047 + match: + as_path: bgp_as3 + evpn: + route_type: multicast + ext_comm: bgp_ext_comm2 + origin: igp + peer: + interface: Ethernet6 + source_protocol: ospf + set: + metric: + rtt_action: subtract + origin: incomplete + - map_name: rm2 + action: permit + sequence_num: 100 + match: + interface: Ethernet16 + set: + as_path_prepend: 200,300,400 + ipv6_next_hop: + global_addr: 37::58 + prefer_global: true + metric: 8000 + - map_name: rm3 + action: deny + sequence_num: 285 + match: + evpn: + route_type: macip + origin: igp + peer: + ipv6: 87:95:15::53 + local_preference: 14783 + source_protocol: bgp + set: + community: + community_attributes: + - no_advertise + metric: + rtt_action: set + origin: igp + call: rm1 + - map_name: rm4 + action: permit + sequence_num: 480 + match: + evpn: + route_type: prefix + source_protocol: static + set: + metric: + rtt_action: subtract + state: overridden + +# After state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn default-route +# match evpn route-type prefix +# match evpn vni 850 +# match ip address prefix-list ip_pfx_list2 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community 35:58 79:150 308:650 45:736 local-AS no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match as-path bgp_as3 +# match evpn route-type multicast +# match ext-community bgp_ext_comm2 +# match peer Ethernet6 +# match source-protocol ospf +# match origin igp +# set metric -rtt +# set origin incomplete +# ! +# route-map rm2 permit 100 +# match interface Ethernet16 +# set as-path prepend 200,300,400 +# set ipv6 next-hop global 37::58 +# set ipv6 next-hop prefer-global +# set metric 8000 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt + + +# Using "deleted" state to remove configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration route-map rm1 80 +# ! +# route-map rm1 permit 80 +# match as-path bgp_as2 +# match evpn default-route +# match evpn route-type prefix +# match evpn vni 850 +# match ip address prefix-list ip_pfx_list2 +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match community bgp_comm_list3 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set as-path prepend 188,257 +# set community 35:58 79:150 308:650 45:736 local-AS no-export no-peer additive +# set extcommunity rt 30:40 +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ------------ +- name: Delete selected route map configuration + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm1 + action: permit + sequence_num: 80 + match: + as_path: bgp_as2 + community: bgp_comm_list3 + evpn: + vni: 850 + ip: + address: ip_pfx_list2 + set: + as_path_prepend: 188,257 + community: + community_number: + - "35:58" + community_attributes: + - local_as + extcommunity: + rt: + - "30:40" + state: deleted + +# After state: +# ------------ +# +# sonic# show running-configuration route-map rm1 80 +# ! +# route-map rm1 permit 80 +# match evpn default-route +# match evpn route-type prefix +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set community 79:150 308:650 45:736 no-export no-peer additive +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 + + +# Using "deleted" state to remove a route map or route map subset +# +# Before state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match evpn default-route +# match evpn route-type prefix +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set community 79:150 308:650 45:736 no-export no-peer additive +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm1 deny 3047 +# match as-path bgp_as3 +# match evpn route-type multicast +# match ext-community bgp_ext_comm2 +# match peer Ethernet6 +# match source-protocol ospf +# match origin igp +# set metric -rtt +# set origin incomplete +# ! +# route-map rm2 permit 100 +# match interface Ethernet16 +# set as-path prepend 200,300,400 +# set metric 8000 +# set ipv6 next-hop prefer-global +# set ipv6 next-hop global 37::58 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt +# ------------ +- name: Delete a route map or route map subset + dellemc.enterprise_sonic.sonic_route_maps: + config: + - map_name: rm1 + sequence_num: 3047 + - map_name: rm2 + sequence_num: 100 + state: deleted + +# After state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match evpn default-route +# match evpn route-type prefix +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set community 79:150 308:650 45:736 no-export no-peer additive +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt + + +# Using "deleted" state to remove all route map configuration +# +# Before state: +# ------------ +# +# sonic# show running-configuration route-map +# ! +# route-map rm1 permit 80 +# match evpn default-route +# match evpn route-type prefix +# match ipv6 address prefix-list ipv6_pfx_list1 +# match interface Vlan7 +# match ext-community bgp_ext_comm1 +# match tag 7284 +# match local-preference 8000 +# match source-vrf Vrf1 +# match ip next-hop prefix-list ip_pfx_list3 +# match peer PortChannel 14 +# match source-protocol bgp +# match metric 400 +# match origin egp +# set community 79:150 308:650 45:736 no-export no-peer additive +# set extcommunity soo 10.73.14.9:78 +# set comm-list bgp_comm_list2 delete +# set metric +rtt +# set ip next-hop 10.48.16.18 +# set ipv6 next-hop global 30::30 +# set local-preference 635 +# set origin egp +# set weight 93471 +# ! +# route-map rm3 deny 285 +# match evpn route-type macip +# match local-preference 14783 +# call rm1 +# match peer 87:95:15::53 +# match source-protocol bgp +# match origin igp +# set community no-advertise +# set metric rtt +# set origin igp +# ! +# route-map rm4 permit 480 +# match evpn route-type prefix +# match source-protocol static +# set metric -rtt +# ------------ +- name: Delete all route map configuration + dellemc.enterprise_sonic.sonic_route_maps: + config: [] + state: deleted + +# After state: +# ------------ +# +# sonic# show running-configuration route-map +# sonic# +# (no route map configuration present) + + +""" +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 + as 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 + as the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['command 1', 'command 2', 'command 3'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.argspec.route_maps.route_maps import Route_mapsArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.route_maps.route_maps import Route_maps + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Route_mapsArgs.argument_spec, + supports_check_mode=True) + + result = Route_maps(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 index 7a528cdf0..b6f8be3b7 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_static_routes.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_static_routes.py @@ -33,6 +33,8 @@ DOCUMENTATION = """ --- module: sonic_static_routes version_added: 2.0.0 +notes: + - Supports C(check_mode). short_description: Manage static routes configuration on SONiC description: - This module provides configuration management of static routes for devices running SONiC @@ -108,6 +110,8 @@ options: choices: - merged - deleted + - overridden + - replaced default: merged """ EXAMPLES = """ @@ -137,13 +141,13 @@ EXAMPLES = """ metric: 2 tag: 4 track: 8 - - vrf_name: '{{vrf_1}}' + - vrf_name: 'VrfReg1' static_list: - prefix: '3.0.0.0/8' next_hops: - index: interface: 'eth0' - nexthop_vrf: '{{vrf_2}}' + nexthop_vrf: 'VrfReg2' next_hop: '4.0.0.0' metric: 4 tag: 5 @@ -162,7 +166,7 @@ EXAMPLES = """ # 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 +# ip route vrf VrfReg1 3.0.0.0/8 blackhole tag 20 track 30 10 # # # Modifying previous merge @@ -170,7 +174,7 @@ EXAMPLES = """ - name: Modify static routes configurations dellemc.enterprise_sonic.sonic_static_routes: config: - - vrf_name: '{{vrf_1}}' + - vrf_name: 'VrfReg1' static_list: - prefix: '3.0.0.0/8' next_hops: @@ -188,7 +192,65 @@ EXAMPLES = """ # 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 +# ip route vrf VrfReg1 3.0.0.0/8 blackhole tag 22 track 33 11 + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip route" +# ip route 4.0.0.0/8 2.0.0.0 tag 4 track 8 2 + + - name: Override static routes configurations + dellemc.enterprise_sonic.sonic_static_routes: + config: + - vrf_name: 'VrfReg2' + static_list: + - prefix: '3.0.0.0/8' + next_hops: + - index: + blackhole: True + metric: 10 + tag: 20 + track: 30 + state: overridden + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip route" +# ip route vrf VrfReg2 3.0.0.0/8 blackhole tag 20 track 30 10 + + +# Using Replaced +# +# Before State: +# ------------- +# +# sonic# show running-configuration | grep "ip route" +# ip route 4.0.0.0/8 2.0.0.0 tag 4 track 8 2 + + - name: Replace static routes configurations + dellemc.enterprise_sonic.sonic_static_routes: + config: + - vrf_name: 'default' + static_list: + - prefix: '4.0.0.0/8' + next_hops: + - index: + blackhole: True + metric: 5 + tag: 10 + track: 15 + state: replaced + +# After State: +# ------------ +# +# sonic# show running-configuration | grep "ip route" +# ip route 4.0.0.0/8 blackhole tag 10 track 15 5 # Using deleted @@ -200,7 +262,7 @@ EXAMPLES = """ # 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 +# 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: @@ -211,7 +273,7 @@ EXAMPLES = """ next_hops: - index: interface: 'Ethernet4' - - vrf_name: '{{vrf_1}}' + - vrf_name: 'VrfReg1' state: deleted # After State: @@ -237,6 +299,13 @@ after: sample: > The configuration returned will always be in the same format of the parameters above. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_stp.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_stp.py new file mode 100644 index 000000000..a25252547 --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_stp.py @@ -0,0 +1,677 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 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_stp +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: sonic_stp +version_added: "2.3.0" +short_description: Manage STP configuration on SONiC +description: + - This module provides configuration management of STP for devices running SONiC +author: "Shade Talabi (@stalabi1)" +options: + config: + description: + - Specifies STP configurations + - I(mstp), I(pvst) and I(rapid_pvst) are mutually exclusive. + type: dict + suboptions: + global: + description: + - Global STP configuration + type: dict + suboptions: + enabled_protocol: + description: + - Specifies the type of STP enabled on the device + type: str + choices: ['mst', 'pvst', 'rapid_pvst'] + loop_guard: + description: + - The loop guard default setting for the bridge + type: bool + default: False + bpdu_filter: + description: + - Enables edge port BPDU filter + type: bool + default: False + disabled_vlans: + description: + - List of disabled STP VLANs. The value of a list item can be a single VLAN ID or a range of VLAN IDs + - separated by '-' or '..'; for example 70-100 or 70..100. + type: list + elements: str + root_guard_timeout: + description: + - Specifies root guard recovery timeout in seconds before the port is moved back to forwarding state + - Range 5-600 + type: int + portfast: + description: + - Enables PortFast globally on all access ports + - Configurable for pvst protocol + type: bool + default: False + hello_time: + description: + - Interval in seconds between periodic transmissions of configuration messages by designated ports + - Range 1-10 + type: int + default: 2 + max_age: + description: + - Maximum age in seconds of the information transmitted by the bridge when it is the root bridge + - Range 6-40 + type: int + default: 20 + fwd_delay: + description: + - Delay in seconds used by STP bridges to transition root and designated ports to forwarding + - Range 4-30 + type: int + default: 15 + bridge_priority: + description: + - The manageable component of the bridge identifier + - Value must be a multiple of 4096 in the range of 0-61440 + type: int + default: 32768 + interfaces: + description: + - Interfaces STP configuration + type: list + elements: dict + suboptions: + intf_name: + description: + - Name of interface + type: str + required: True + edge_port: + description: + - Configure interface as an STP edge port + type: bool + default: False + link_type: + description: + - Specifies the interface's link type + type: str + choices: ['point-to-point', 'shared'] + guard: + description: + - Enables root guard or loop guard + type: str + choices: ['loop', 'root', 'none'] + bpdu_guard: + description: + - Enable edge port BPDU guard + type: bool + default: False + bpdu_filter: + description: + - Enables edge port BPDU filter + type: bool + default: False + portfast: + description: + - Enable/Disable portfast on specified interface + - Configurable for pvst protocol + type: bool + default: False + uplink_fast: + description: + - Enables uplink fast + type: bool + default: False + shutdown: + description: + - Port to be shutdown when it receives a BPDU + type: bool + default: False + cost: + description: + - The port's contribution, when it is the root port, to the root path cost for the bridge + type: int + port_priority: + description: + - The manageable component of the port identifier + - Range 0-240 + type: int + stp_enable: + description: + - Enables STP on the interface + type: bool + default: True + mstp: + description: + - Multi STP configuration + type: dict + suboptions: + mst_name: + description: + - Name of the MST configuration identifier + type: str + revision: + description: + - Revision level of the MST configuration identifier + type: int + max_hop: + description: + - Number of bridges in an MST region that a BPDU can traverse before it is discarded + type: int + hello_time: + description: + - Interval in seconds between periodic transmissions of configuration messages by designated ports + - Range 1-10 + type: int + max_age: + description: + - Maximum age in seconds of the information transmitted by the bridge when it is the root bridge + - Range 6-40 + type: int + fwd_delay: + description: + - Delay in seconds used by STP bridges to transition root and designated ports to forwarding + - Range 4-30 + type: int + mst_instances: + description: + - Configuration for MST instances + type: list + elements: dict + suboptions: + mst_id: + description: + - Value used to identify MST instance + type: int + required: True + bridge_priority: + description: + - The manageable component of the bridge identifier + - Value must be a multiple of 4096 + type: int + vlans: + description: + - List of VLANs mapped to the MST instance. The value of a list item can be a single VLAN ID or a range of VLAN IDs + - separated by '-' or '..'; for example 70-100 or 70..100. + type: list + elements: str + interfaces: + description: + - List of STP enabled interfaces + type: list + elements: dict + suboptions: + intf_name: + description: + - Reference to the STP interface + type: str + required: True + cost: + description: + - The port's contribution, when it is the root port, to the root path cost for the bridge + type: int + port_priority: + description: + - The manageable component of the port identifier + type: int + pvst: + description: + - Per VLAN STP configuration + type: list + elements: dict + suboptions: + vlan_id: + description: + - VLAN identifier + type: int + required: True + hello_time: + description: + - Interval in seconds between periodic transmissions of configuration messages by designated ports + - Range 1-10 + type: int + max_age: + description: + - Maximum age in seconds of the information transmitted by the bridge when it is the root bridge + - Range 6-40 + type: int + fwd_delay: + description: + - Delay in seconds used by STP bridges to transition root and designated ports to forwarding + - Range 4-30 + type: int + bridge_priority: + description: + - The manageable component of the bridge identifier + - Value must be a multiple of 4096 + type: int + interfaces: + description: + - List of STP enabled interfaces + type: list + elements: dict + suboptions: + intf_name: + description: + - Reference to the STP interface + type: str + required: True + cost: + description: + - The port's contribution, when it is the root port, to the root path cost for the bridge + type: int + port_priority: + description: + - The manageable component of the port identifier + type: int + rapid_pvst: + description: + - Rapid per VLAN STP configuration + type: list + elements: dict + suboptions: + vlan_id: + description: + - VLAN identifier + type: int + required: True + hello_time: + description: + - Interval in seconds between periodic transmissions of configuration messages by designated ports + - Range 1-10 + type: int + max_age: + description: + - Maximum age in seconds of the information transmitted by the bridge when it is the root bridge + - Range 6-40 + type: int + fwd_delay: + description: + - Delay in seconds used by STP bridges to transition root and designated ports to forwarding + - Range 4-30 + type: int + bridge_priority: + description: + - The manageable component of the bridge identifier + - Value must be a multiple of 4096 + type: int + interfaces: + description: + - List of STP enabled interfaces + type: list + elements: dict + suboptions: + intf_name: + description: + - Reference to the STP interface + type: str + required: True + cost: + description: + - The port's contribution, when it is the root port, to the root path cost for the bridge + type: int + port_priority: + description: + - The manageable component of the port identifier + type: int + state: + description: + - The state of the configuration after module completion + type: str + choices: ['merged', 'deleted', 'replaced', 'overridden'] + default: merged +""" +EXAMPLES = """ + +# Using merged +# +# Before State: +# ------------- +# +# sonic# show running-configuration spanning-tree +# (No spanning-tree configuration present) + +- name: Merge STP configurations + dellemc.enterprise_sonic.sonic_stp: + config: + global: + enabled_protocol: mst + loop_guard: true + bpdu_filter: true + disabled_vlans: + - 4-6 + hello_time: 5 + max_age: 10 + fwd_delay: 20 + bridge_priority: 4096 + interfaces: + - intf_name: Ethernet20 + edge_port: true + link_type: shared + guard: loop + bpdu_guard: true + bpdu_filter: true + uplink_fast: true + shutdown: true + cost: 20 + port_priority: 30 + stp_enable: true + mstp: + mst_name: mst1 + revision: 1 + max_hop: 3 + hello_time: 6 + max_age: 9 + fwd_delay: 12 + mst_instances: + - mst_id: 1 + bridge_priority: 2048 + vlans: + - 1 + interfaces: + - intf_name: Ethernet20 + cost: 60 + port_priority: 65 + state: merged + +# After State: +# ------------ +# +# sonic# show running-configuration spanning-tree +# no spanning-tree vlan 4-6 +# spanning-tree mode mst +# spanning-tree edge-port bpdufilter default +# spanning-tree forward-time 20 +# spanning-tree hello-time 5 +# spanning-tree max-age 10 +# spanning-tree loopguard default +# spanning-tree mst hello-time 6 +# spanning-tree mst forward-time 12 +# spanning-tree mst max-age 9 +# spanning-tree mst max-hops 3 +# spanning-tree mst 1 priority 2048 +# ! +# spanning-tree mst configuration +# name mst1 +# revision 1 +# instance 1 vlan 1 +# activate +# ! +# interface Ethernet20 +# spanning-tree bpdufilter enable +# spanning-tree guard loop +# spanning-tree bpduguard port-shutdown +# spanning-tree cost 20 +# spanning-tree link-type shared +# spanning-tree port-priority 30 +# spanning-tree port type edge +# spanning-tree uplinkfast +# spanning-tree mst 1 cost 60 +# spanning-tree mst 1 port-priority 65 + + +# Using replaced +# +# Before State: +# ------------- +# +# sonic# show running-configuration spanning-tree +# no spanning-tree vlan 4-6 +# spanning-tree mode mst +# spanning-tree edge-port bpdufilter default +# spanning-tree loopguard default +# spanning-tree mst hello-time 6 +# spanning-tree mst forward-time 12 +# spanning-tree mst max-age 9 +# spanning-tree mst max-hops 3 +# spanning-tree mst 1 priority 2048 +# ! +# spanning-tree mst configuration +# name mst1 +# revision 1 +# instance 1 vlan 1 +# activate +# ! +# interface Ethernet20 +# spanning-tree bpdufilter enable +# spanning-tree guard loop +# spanning-tree bpduguard port-shutdown +# spanning-tree cost 20 +# spanning-tree link-type shared +# spanning-tree port-priority 30 +# spanning-tree port type edge +# spanning-tree uplinkfast +# spanning-tree mst 1 cost 60 +# spanning-tree mst 1 port-priority 65 + +- name: Replace STP configurations + dellemc.enterprise_sonic.sonic_stp: + config: + interfaces: + - intf_name: Ethernet20 + cost: 25 + port_priority: 35 + mstp: + mst_name: mst2 + revision: 2 + max_hop: 4 + hello_time: 7 + max_age: 10 + fwd_delay: 13 + state: replaced + +# After State: +# ------------ +# +# sonic# show running-configuration spanning-tree +# no spanning-tree vlan 4-6 +# spanning-tree mode mst +# spanning-tree edge-port bpdufilter default +# spanning-tree loopguard default +# spanning-tree mst hello-time 7 +# spanning-tree mst forward-time 13 +# spanning-tree mst max-age 10 +# spanning-tree mst max-hops 4 +# ! +# spanning-tree mst configuration +# name mst2 +# revision 2 +# activate +# ! +# interface Ethernet20 +# spanning-tree cost 25 +# spanning-tree port-priority 35 + + +# Using overridden +# +# Before State: +# ------------- +# +# sonic# show running-configuration spanning-tree +# no spanning-tree vlan 4-6 +# spanning-tree mode mst +# spanning-tree edge-port bpdufilter default +# spanning-tree loopguard default +# spanning-tree mst hello-time 7 +# spanning-tree mst forward-time 13 +# spanning-tree mst max-age 10 +# spanning-tree mst max-hops 4 +# ! +# spanning-tree mst configuration +# name mst2 +# revision 2 +# activate +# ! +# interface Ethernet20 +# spanning-tree cost 25 +# spanning-tree port-priority 35 + +- name: Override STP configurations + dellemc.enterprise_sonic.sonic_stp: + config: + global: + enabled_protocol: pvst + bpdu_filter: true + root_guard_timeout: 25 + portfast: true + hello_time: 5 + max_age: 10 + fwd_delay: 20 + bridge_priority: 4096 + pvst: + - vlan_id: 1 + hello_time: 4 + max_age: 6 + fwd_delay: 8 + bridge_priority: 4096 + interfaces: + - intf_name: Ethernet20 + cost: 10 + port_priority: 50 + state: overridden + +# After State: +# ------------ +# +# sonic# show running-configuration spanning-tree +# spanning-tree mode pvst +# spanning-tree edge-port bpdufilter default +# spanning-tree forward-time 20 +# spanning-tree guard root timeout 25 +# spanning-tree hello-time 5 +# spanning-tree max-age 10 +# spanning-tree priority 4096 +# spanning-tree portfast default +# spanning-tree vlan 1 hello-time 4 +# spanning-tree vlan 1 forward-time 8 +# spanning-tree vlan 1 max-age 6 +# sonic# show running-configuration interface Ethernet 20 | grep spanning-tree +# spanning-tree vlan 1 cost 10 +# spanning-tree vlan 1 port-priority 50 + + +# Using deleted +# +# Before State: +# ------------- +# +# sonic# show running-configuration spanning-tree +# spanning-tree mode pvst +# spanning-tree edge-port bpdufilter default +# spanning-tree forward-time 20 +# spanning-tree guard root timeout 25 +# spanning-tree hello-time 5 +# spanning-tree max-age 10 +# spanning-tree priority 4096 +# spanning-tree portfast default +# spanning-tree vlan 1 hello-time 4 +# spanning-tree vlan 1 forward-time 8 +# spanning-tree vlan 1 max-age 6 +# sonic# show running-configuration interface Ethernet 20 | grep spanning-tree +# spanning-tree vlan 1 cost 10 +# spanning-tree vlan 1 port-priority 50 + +- name: Delete STP configurations + dellemc.enterprise_sonic.sonic_stp: + config: + global: + bpdu_filter: true + root_guard_timeout: 25 + pvst: + - vlan_id: 1 + interfaces: + - intf_name: Ethernet20 + state: deleted + +# After State: +# ------------ +# +# sonic# show running-configuration spanning-tree +# spanning-tree mode pvst +# spanning-tree forward-time 20 +# spanning-tree hello-time 5 +# spanning-tree max-age 10 +# spanning-tree priority 4096 +# spanning-tree portfast default +# spanning-tree vlan 1 hello-time 4 +# spanning-tree vlan 1 forward-time 8 +# spanning-tree vlan 1 max-age 6 +# sonic# show running-configuration interface Ethernet 20 | grep spanning-tree +# (No spanning-tree configuration present) + + +""" +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.stp.stp import StpArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.stp.stp import Stp + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=StpArgs.argument_spec, + supports_check_mode=True) + + result = Stp(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 index efb285a11..8b4d29ae1 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_system.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_system.py @@ -80,7 +80,7 @@ options: - 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'] + choices: ['merged', 'replaced', 'overridden', 'deleted'] type: str """ EXAMPLES = """ @@ -167,6 +167,89 @@ EXAMPLES = """ #ipv6 anycast-address enable #interface-naming standard +# Using replaced +# +# 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 + +- name: Replace system configuration. + sonic_system: + config: + hostname: sonic + interface_naming: standard + state: replaced + +# After state: +# ------------ +#! +#SONIC(config)#do show running-configuration +#! +#interface-naming standard + +# Using replaced +# +# Before state: +# ------------- +#! +#sonic(config)#do show running-configuration +#! +#ip anycast-mac-address aa:bb:cc:dd:ee:ff +#interface-naming standard + +- name: Replace system device configuration. + sonic_system: + config: + hostname: sonic + interface_naming: standard + anycast_address: + ipv6: true + ipv4: true + state: replaced + +# After state: +# ------------ +#! +#SONIC(config)#do show running-configuration +#! +#ip anycast-address enable +#ipv6 anycast-address enable +#interface-naming standard + +# Using overridden +# +# 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 + +- name: Override system configuration. + sonic_system: + config: + hostname: sonic + interface_naming: standard + anycast_address: + ipv4: true + mac_address: bb:aa:cc:dd:ee:ff + state: overridden + +# After state: +# ------------ +#! +#SONIC(config)#do show running-configuration +#! +#ip anycast-mac-address bb:aa:cc:dd:ee:ff +#ip anycast-address enable +#interface-naming standard """ RETURN = """ 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 index 3295e11ba..3361345f5 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_tacacs_server.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_tacacs_server.py @@ -64,6 +64,7 @@ options: description: - Specifies the timeout of the tacacs server. type: int + default: 5 source_interface: description: - Specifies the source interface of the tacacs server. @@ -122,8 +123,10 @@ options: - 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. + - In case of replaced, the existing tacacs server configuration will be replaced with provided configuration. + - In case of overridden, the existing tacacs server configuration will be overridden with the provided configuration. default: merged - choices: ['merged', 'deleted'] + choices: ['merged', 'replaced', 'overridden', 'deleted'] type: str """ EXAMPLES = """ @@ -249,8 +252,110 @@ EXAMPLES = """ #HOST AUTH-TYPE KEY PORT PRIORITY TIMEOUT VRF #------------------------------------------------------------------------------------------------ #1.2.3.4 pap 1234 49 1 5 default - - +# +# Using replaced +# +# Before state: +# ------------- +# +#sonic(config)# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#source-interface : Ethernet12 +#timeout : 10 +#auth-type : pap +#key configured : Yes +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG PORT PRIORITY TIMEOUT VRF +#-------------------------------------------------------------------------------------- +#1.2.3.4 pap No 49 1 5 default +# +- name: Replace tacacs configurations + sonic_tacacs_server: + config: + auth_type: pap + key: pap + source_interface: Ethernet12 + timeout: 10 + servers: + - host: + name: 1.2.3.4 + auth_type: mschap + key: 1234 + state: replaced +# +# After state: +# ------------ +# +#sonic(config)# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#source-interface : Ethernet12 +#timeout : 10 +#auth-type : pap +#key configured : Yes +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG PORT PRIORITY TIMEOUT VRF +#-------------------------------------------------------------------------------------- +#1.2.3.4 mschap Yes 49 1 5 default +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic(config)# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#source-interface : Ethernet12 +#timeout : 10 +#auth-type : pap +#key configured : Yes +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG PORT PRIORITY TIMEOUT VRF +#-------------------------------------------------------------------------------------- +#1.2.3.4 pap No 49 1 5 default +#11.12.13.14 chap Yes 49 10 5 default +# +- name: Override tacacs configurations + sonic_tacacs_server: + config: + auth_type: mschap + key: mschap + source_interface: Ethernet12 + timeout: 20 + servers: + - host: + name: 1.2.3.4 + auth_type: mschap + key: mschap + - host: + name: 10.10.11.12 + auth_type: chap + timeout: 30 + priority: 2 + state: overridden +# +# After state: +# ------------ +# +#sonic(config)# do show tacacs-server +#--------------------------------------------------------- +#TACACS Global Configuration +#--------------------------------------------------------- +#source-interface : Ethernet12 +#timeout : 20 +#auth-type : mschap +#key configured : Yes +#-------------------------------------------------------------------------------------- +#HOST AUTH-TYPE KEY-CONFIG PORT PRIORITY TIMEOUT VRF +#-------------------------------------------------------------------------------------- +#1.2.3.4 mschap Yes 49 1 5 default +#10.10.11.12 chap No 49 2 30 default +# """ RETURN = """ before: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_users.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_users.py index 7f0855a94..ac528e88d 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_users.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_users.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright 2019 Red Hat +# Copyright 2023 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) @@ -59,6 +59,8 @@ options: choices: - admin - operator + - netadmin + - secadmin password: description: - Specifies the password of the user. @@ -78,8 +80,10 @@ options: - 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. + - In case of replaced, the existing specified user configuration will be replaced with provided configuration. + - In case of overridden, the existing users configuration will be overridden with the provided configuration. default: merged - choices: ['merged', 'deleted'] + choices: ['merged', 'deleted', 'overridden', 'replaced'] type: str """ EXAMPLES = """ @@ -88,38 +92,44 @@ EXAMPLES = """ # 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 +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# sysadmin admin +# sysoperator operator + +- name: Delete user 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 - +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# sysadmin 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 +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# sysadmin admin +# sysoperator operator + +- name: Delete all users configurations except admin dellemc.enterprise_sonic.sonic_users: config: state: deleted @@ -127,20 +137,23 @@ EXAMPLES = """ # After state: # ------------ # -#do show running-configuration -#! -#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin - +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin # Using merged # # Before state: # ------------- # -#do show running-configuration -#! -#username admin password $6$sdZt2C7F$3oPSRkkJyLZtsKlFNGWdwssblQWBj5dXM6qAJAQl7dgOfqLSpZJ/n6xf8zPRcqPUFCu5ZKpEtynJ9sZ/S8Mgj. role admin -# +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin + - name: Merge users configurations dellemc.enterprise_sonic.sonic_users: config: @@ -156,14 +169,83 @@ EXAMPLES = """ # 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 +# +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# sysadmin admin +# sysoperator operator + +# Using Overridden +# +# Before state: +# ------------- +# +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# sysadmin admin +# sysoperator operator + +- name: Override users configurations + dellemc.enterprise_sonic.sonic_users: + config: + - name: user1 + role: secadmin + password: 123abc + update_password: always + state: overridden + +# After state: +# ------------ +# +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# user1 secadmin +# Using Replaced +# +# Before state: +# ------------- +# +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# user1 secadmin +# user2 operator +- name: Replace users configurations + dellemc.enterprise_sonic.sonic_users: + config: + - name: user1 + role: operator + password: 123abc + update_password: always + - name: user2 + role: netadmin + password: 123abc + update_password: always + state: replaced + +# After state: +# ------------ +# +# sonic# show users configured +# ---------------------------------------------------------------------- +# User Role(s) +# ---------------------------------------------------------------------- +# admin admin +# user1 operator +# user2 netadmin """ RETURN = """ before: @@ -180,6 +262,13 @@ after: sample: > The configuration returned will always be in the same format of the parameters above. +after(generated): + description: The generated configuration model invocation. + returned: when C(check_mode) + 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 diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlan_mapping.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlan_mapping.py new file mode 100644 index 000000000..985e0523b --- /dev/null +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlan_mapping.py @@ -0,0 +1,543 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2023 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_vlan_mapping +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community', + 'license': 'Apache 2.0' +} + +DOCUMENTATION = """ +--- +module: sonic_vlan_mapping +author: "Cypher Miller (@Cypher-Miller)" +version_added: "2.1.0" +short_description: Configure vlan mappings on SONiC. +description: + - This module provides configuration management for vlan mappings on devices running SONiC. + - Vlan mappings only available on TD3 and TD4 devices. + - For TD4 devices must enable vlan mapping first (can enable in config-switch-resource). +options: + config: + description: + - Specifies the vlan mapping related configurations. + type: list + elements: dict + suboptions: + name: + description: + - Full name of the interface, i.e. Ethernet8, PortChannel2, Eth1/2. + required: true + type: str + mapping: + description: + - Defining a single vlan mapping. + type: list + elements: dict + suboptions: + service_vlan: + description: + - Configure service provider VLAN ID. + - VLAN ID range is 1-4094. + required: true + type: int + vlan_ids: + description: + - Configure customer VLAN IDs. + - If mode is double tagged translation then this VLAN ID represents the outer VLAN ID. + - If mode is set to stacking can pass ranges and/or multiple list entries. + - Individual VLAN ID or (-) separated range of VLAN IDs. + type: list + elements: str + dot1q_tunnel: + description: + - Specify whether it is a vlan stacking or translation (false means translation; true means stacking). + type: bool + default: false + inner_vlan: + description: + - Configure inner customer VLAN ID. + - VLAN IDs range is 1-4094. + - Only available for double tagged translations. + type: int + priority: + description: + - Set priority level of the vlan mapping. + - Priority range is 0-7. + type: int + state: + description: + - Specifies the operation to be performed on the vlan mappings configured on the device. + - In case of merged, the input configuration will be merged with the existing vlan mappings on the device. + - In case of deleted, the existing vlan mapping configuration will be removed from the device. + - In case of overridden, all existing vlan mappings will be deleted and the specified input configuration will be add. + - In case of replaced, the existing vlan mappings on the device will be replaced by the configuration for each vlan mapping. + type: str + default: merged + choices: + - merged + - deleted + - replaced + - overridden +""" +EXAMPLES = """ +# Using deleted +# +# Before State: +# ------------- +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +# switchport vlan-mapping 392 inner 590 2755 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400-402,404,406,408,410,412,420,422,430-432 dot1q-tunnel 2436 priority 3 +# switchport vlan-mapping 300 dot1q-tunnel 2567 priority 3 +#! + + + - name: Delete vlan mapping configurations + sonic_vlan_mapping: + config: + - name: Ethernet8 + mapping: + - service_vlan: 2755 + - name: Ethernet16 + mapping: + - service_vlan: 2567 + priority: 3 + - service_vlan: 2436 + vlan_ids: + - 404 + - 401 + - 412 + - 430-431 + priority: 3 + state: deleted + +# After State: +# ------------ +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400,402,406,408,410,420,422,432 dot1q-tunnel 2436 +# switchport vlan-mapping 300 dot1q-tunnel 2567 +#! + + +# Using deleted +# +# Before State: +# ------------- +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +# switchport vlan-mapping 392 inner 590 2755 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400-402,404,406,408,410,412,420,422,430-431 dot1q-tunnel 2436 +# switchport vlan-mapping 300 dot1q-tunnel 2567 priority 3 +#! + + + - name: Delete vlan mapping configurations + sonic_vlan_mapping: + config: + - name: Ethernet8 + - name: Ethernet16 + mapping: + - service_vlan: 2567 + state: deleted + +# After State: +# ------------ +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdo#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400-402,406,408,410,420,422,431 dot1q-tunnel 2436 +#! + + +# Using merged +# +# Before State: +# ------------- +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +#! +#interface PortChannel 2 +# switchport vlan-mapping 345 2999 priority 0 +# switchport vlan-mapping 500,540 dot1q-tunnel 3000 +# no shutdown +#! + + - name: Add vlan mapping configurations + sonic_vlan_mapping: + config: + - name: Ethernet8 + mapping: + - service_vlan: 2755 + vlan_ids: + - 392 + dot1q_tunnel: false + inner_vlan: 590 + - name: Ethernet16 + mapping: + - service_vlan: 2567 + vlan_ids: + - 300 + dot1q_tunnel: true + priority: 3 + - service_vlan: 2436 + vlan_ids: + - 400-402 + - 404 + - 406 + - 408 + - 410 + - 412 + - 420 + - 422 + - 430-431 + dot1q_tunnel: true + - name: Portchannel 2 + mapping: + - service_vlan: 2999 + priority: 4 + - service_vlan: 3000 + vlan_ids: + - 506-512 + - 561 + priority: 5 + state: merged + +# After State: +# ------------ +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +# switchport vlan-mapping 392 inner 590 2755 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400-402,404,406,408,410,412,420,422,430-431 dot1q-tunnel 2436 +# switchport vlan-mapping 300 dot1q-tunnel 2567 priority 3 +#! +#interface PortChannel 2 +# switchport vlan-mapping 345 2999 priority 4 +# switchport vlan-mapping 500,506-512,540,561 dot1q-tunnel 3000 priority 5 +# no shutdown +#! + + +# Using replaced +# +# Before State: +# ------------- +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +# switchport vlan-mapping 392 inner 590 2755 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400-402,404,406,408,410,412,420,422,430-431 dot1q-tunnel 2436 +# switchport vlan-mapping 300 dot1q-tunnel 2567 priority 3 +#! +#interface PortChannel 2 +# switchport vlan-mapping 345 2999 priority 0 +# no shutdown +#! + + - name: Replace vlan mapping configurations + sonic_vlan_mapping: + config: + - name: Ethernet8 + mapping: + - service_vlan: 2755 + vlan_ids: + - 390 + dot1q_tunnel: false + inner_vlan: 593 + - name: Ethernet16 + mapping: + - service_vlan: 2567 + vlan_ids: + - 310 + - 330-340 + priority: 5 + - name: Portchannel 2 + mapping: + - service_vlan: 2999 + vlan_ids: + - 345 + dot1q_tunnel: true + priority: 1 + state: replaced + + +# After State: +# ------------ +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +# switchport vlan-mapping 390 inner 593 2755 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400-402,404,406,408,410,412,420,422,430-431 dot1q-tunnel 2436 +# switchport vlan-mapping 310,330-340 dot1q-tunnel 2567 priority 5 +#! +#interface PortChannel 2 +# switchport vlan-mapping 345 dot1q_tunnel 2999 priority 1 +# no shutdown +#! + + +# Using overridden +# +# Before State: +# ------------- +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 623 2411 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 400-402,404,406,408,410,412,420,422,430-431 dot1q-tunnel 2436 +#! + + - name: Override the vlan mapping configurations + sonic_vlan_mapping: + config: + - name: Ethernet8 + mapping: + - service_vlan: 2755 + vlan_ids: + - 392 + dot1q_tunnel: false + inner_vlan: 590 + - name: Ethernet16 + mapping: + - service_vlan: 2567 + vlan_ids: + - 300 + dot1q_tunnel: true + priority: 3 + - name: Portchannel 2 + mapping: + - service_vlan: 2999 + vlan_ids: + - 345 + priority: 0 + state: overridden + +# After State: +# ------------ +# +#sonic# show running-configuration interface +#! +#interface Ethernet8 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 392 inner 590 2755 +#! +#interface Ethernet16 +# mtu 9100 +# speed 400000 +# fec RS +# unreliable-los auto +# shutdown +# switchport vlan-mapping 300 dot1q-tunnel 2567 priority 3 +#! +#interface PortChannel 2 +# switchport vlan-mapping 345 2999 priority 0 +# no shutdown +#! + + +""" +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.vlan_mapping.vlan_mapping import Vlan_mappingArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.vlan_mapping.vlan_mapping import Vlan_mapping + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Vlan_mappingArgs.argument_spec, + supports_check_mode=True) + + result = Vlan_mapping(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 index cfd536c79..cd3d7729d 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlans.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vlans.py @@ -63,6 +63,8 @@ options: type: str choices: - merged + - replaced + - overridden - deleted default: merged """ @@ -109,6 +111,64 @@ EXAMPLES = """ #sonic# # +# Using replaced + +# Before state: +# ------------- +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#30 Inactive +# +#sonic# + +- name: Replace all attributes of specified VLANs with provided configuration + dellemc.enterprise_sonic.sonic_vlans: + config: + - vlan_id: 10 + state: replaced + +# After state: +# ------------ +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#30 Inactive +# +#sonic# + +# Using overridden + +# Before state: +# ------------- +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +#30 Inactive +# +#sonic# + +- name: Override device configuration of all VLANs with provided configuration + dellemc.enterprise_sonic.sonic_vlans: + config: + - vlan_id: 10 + state: overridden + +# After state: +# ------------ +# +#sonic# show Vlan +#Q: A - Access (Untagged), T - Tagged +#NUM Status Q Ports +#10 Inactive +# +#sonic# # Using deleted diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vrfs.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vrfs.py index 4c881aee6..84233145a 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vrfs.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vrfs.py @@ -66,6 +66,8 @@ options: type: str choices: - merged + - replaced + - overridden - deleted default: merged """ @@ -158,6 +160,88 @@ EXAMPLES = """ #Vrfcheck4 Eth1/5 # Eth1/6 # +# Using overridden +# +# Before state: +# ------------- +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 +#Vrfcheck2 +#Vrfcheck3 Eth1/7 +# Eth1/8 +# +- name: Overridden VRF configuration + dellemc.enterprise_sonic.sonic_vrfs: + sonic_vrfs: + config: + - name: Vrfcheck1 + members: + interfaces: + - name: Eth1/3 + - name: Eth1/14 + - name: Vrfcheck3 + members: + interfaces: + - name: Eth1/5 + - name: Eth1/6 + state: overridden +# +# After state: +# ------------ +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 Eth1/3 +# Eth1/14 +#Vrfcheck2 +#Vrfcheck3 Eth1/5 +# Eth1/6 +# +# Using replaced +# +# Before state: +# ------------- +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 Eth1/3 +#Vrfcheck2 +#Vrfcheck3 Eth1/5 +# Eth1/6 +# +- name: Replace VRF configuration + dellemc.enterprise_sonic.sonic_vrfs: + sonic_vrfs: + config: + - name: Vrfcheck1 + members: + interfaces: + - name: Eth1/3 + - name: Eth1/14 + - name: Vrfcheck3 + members: + interfaces: + - name: Eth1/5 + - name: Eth1/6 + state: replaced +# +# After state: +# ------------ +# +#show ip vrf +#VRF-NAME INTERFACES +#---------------------------------------------------------------- +#Vrfcheck1 Eth1/3 +# Eth1/14 +#Vrfcheck2 +#Vrfcheck3 Eth1/5 +# Eth1/6 +# """ RETURN = """ before: diff --git a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vxlans.py b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vxlans.py index e6613ba24..0500db79e 100644 --- a/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vxlans.py +++ b/ansible_collections/dellemc/enterprise_sonic/plugins/modules/sonic_vxlans.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# © Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved +# © Copyright 2023 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) @@ -43,7 +43,6 @@ options: config: description: - A list of VxLAN configurations. - - source_ip and evpn_nvo are required together. type: list elements: dict suboptions: @@ -90,6 +89,8 @@ options: choices: - merged - deleted + - replaced + - overridden default: merged """ EXAMPLES = """ @@ -173,7 +174,7 @@ EXAMPLES = """ - name: vteptest1 source_ip: 1.1.1.1 primary_ip: 2.2.2.2 - evpn_nvo_name: nvo1 + evpn_nvo: nvo1 vlan_map: - vni: 101 vlan: 11 @@ -199,7 +200,87 @@ EXAMPLES = """ # map vni 101 vrf Vrfcheck1 # map vni 102 vrf Vrfcheck2 #! +# +# Using overridden +# +# 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 overridden state 01" + dellemc.enterprise_sonic.sonic_vxlans: + config: + - name: vteptest2 + source_ip: 3.3.3.3 + primary_ip: 4.4.4.4 + evpn_nvo: nvo2 + vlan_map: + - vni: 101 + vlan: 11 + vrf_map: + - vni: 101 + vrf: Vrfcheck1 + state: overridden +# +# After state: +# ------------ +# +# do show running-configuration +# +#interface vxlan vteptest2 +# source-ip 3.3.3.3 +# primary-ip 4.4.4.4 +# map vni 101 vlan 11 +# map vni 101 vrf Vrfcheck1 +#! +# +# Using replaced +# +# Before state: +# ------------- +# +# do show running-configuration +# +#interface vxlan vteptest2 +# source-ip 3.3.3.3 +# primary-ip 4.4.4.4 +# map vni 101 vlan 11 +# map vni 101 vrf Vrfcheck +#! +# +- name: "Test vxlans replaced state 01" + dellemc.enterprise_sonic.sonic_vxlans: + config: + - name: vteptest2 + source_ip: 5.5.5.5 + vlan_map: + - vni: 101 + vlan: 12 + state: replaced +# +# After state: +# ------------ +# +# do show running-configuration +# +#interface vxlan vteptest2 +# source-ip 5.5.5.5 +# primary-ip 4.4.4.4 +# map vni 101 vlan 12 +# map vni 101 vrf Vrfcheck1 +#! # """ + RETURN = """ before: description: The configuration prior to the model invocation. |