diff --git a/changelogs/fragments/415-sonic-mirroring-module.yaml b/changelogs/fragments/415-sonic-mirroring-module.yaml new file mode 100644 index 000000000..64d58b02b --- /dev/null +++ b/changelogs/fragments/415-sonic-mirroring-module.yaml @@ -0,0 +1,2 @@ +major_changes: + - sonic_mirroring - Add 'sonic_mirroring' module to Dell enterprise SONiC collection (https://github.com/ansible-collections/dellemc.enterprise_sonic/pull/415). diff --git a/plugins/module_utils/network/sonic/argspec/facts/facts.py b/plugins/module_utils/network/sonic/argspec/facts/facts.py index 2de26c4e4..cd832cf1a 100644 --- a/plugins/module_utils/network/sonic/argspec/facts/facts.py +++ b/plugins/module_utils/network/sonic/argspec/facts/facts.py @@ -75,7 +75,8 @@ def __init__(self, **kwargs): 'pim_global', 'pim_interfaces', 'login_lockout', - 'poe' + 'poe', + 'mirroring' ] argument_spec = { diff --git a/plugins/module_utils/network/sonic/argspec/mirroring/mirroring.py b/plugins/module_utils/network/sonic/argspec/mirroring/mirroring.py new file mode 100644 index 000000000..ea31b9241 --- /dev/null +++ b/plugins/module_utils/network/sonic/argspec/mirroring/mirroring.py @@ -0,0 +1,79 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The arg spec for the sonic_mirroring module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class MirroringArgs(object): # pylint: disable=R0903 + + """The arg spec for the sonic_mirroring module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "options": { + "span": { + "elements": "dict", + "options": { + "name": {"required": True, "type": "str"}, + "dst_port": {"type": "str"}, + "source": {"type": "str"}, + "direction": {"choices": ["rx", "tx", "both"], + "type": "str"} + }, + "type": "list" + }, + "erspan": { + "elements": "dict", + "options": { + "name": {"required": True, "type": "str"}, + "dst_ip": {"type": "str"}, + "src_ip": {"type": "str"}, + "source": {"type": "str"}, + "direction": {"choices": ["rx", "tx", "both"], + "type": "str"}, + "dscp": {"type": "int"}, + "gre": {"type": "str"}, + "ttl": {"type": "int"}, + "queue": {"type": "int"} + }, + "type": "list" + } + }, + "type": "dict" + }, + "state": { + "choices": ["merged", "replaced", "overridden", "deleted"], + "default": "merged", + "type": "str" + } + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/sonic/config/mirroring/mirroring.py b/plugins/module_utils/network/sonic/config/mirroring/mirroring.py new file mode 100644 index 000000000..cb4051612 --- /dev/null +++ b/plugins/module_utils/network/sonic/config/mirroring/mirroring.py @@ -0,0 +1,487 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic_mirroring 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.module_utils.connection import ConnectionError +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import Facts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import ( + to_request, + edit_config +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import ( + update_states, + get_diff, + get_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, + get_new_config, + get_formatted_config_diff +) + +PATCH = 'patch' +DELETE = 'delete' +URL = 'data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST' +TEST_KEYS = [ + {'span': {'name': ''}}, + {'erspan': {'name': ''}}, +] + + +def __preprocess_mirroring_config_for_merge_op(key_set, command, have): + hv = remove_empties(have) + cmd = remove_empties(command) + if not hv or not cmd: + return False, hv + + h_span = hv.get('span', []) + h_erspan = hv.get('erspan', []) + + c_span = cmd.get('span', []) + c_erspan = cmd.get('erspan', []) + + for ms in c_span: + name = ms['name'] + for h_ms in h_erspan[:]: + if name == h_ms['name']: + h_erspan.remove(h_ms) + break + + for ms in c_erspan: + name = ms['name'] + for h_ms in h_span[:]: + if name == h_ms['name']: + h_span.remove(h_ms) + break + + new_have = remove_empties(hv) + return False, new_have + + +delete_all = False +TEST_KEYS_generate_config = [ + {'config': {'__merge_op': __preprocess_mirroring_config_for_merge_op}}, + {'span': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, + {'erspan': {'name': '', '__delete_op': __DELETE_CONFIG_IF_NO_SUBCONFIG}}, +] + + +class Mirroring(ConfigBase): + """ + The sonic_mirroring class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'mirroring', + ] + + def __init__(self, module): + super(Mirroring, self).__init__(module) + + def get_mirroring_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) + mirroring_facts = facts['ansible_network_resources'].get('mirroring') + if not mirroring_facts: + return {} + return mirroring_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_mirroring_facts = self.get_mirroring_facts() + commands, requests = self.set_config(existing_mirroring_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_mirroring_facts = self.get_mirroring_facts() + + result['before'] = existing_mirroring_facts + if result['changed']: + result['after'] = changed_mirroring_facts + + new_config = changed_mirroring_facts + old_config = existing_mirroring_facts + if self._module.check_mode: + result.pop('after', None) + new_config = get_new_config(commands, old_config, + TEST_KEYS_generate_config) + new_config = remove_empties(new_config) + result['after(generated)'] = new_config + + if self._module._diff: + self.sort_mirrors(old_config) + self.sort_mirrors(new_config) + result['diff'] = get_formatted_config_diff(old_config, + new_config, + self._module._verbosity) + result['warnings'] = warnings + return result + + def set_config(self, existing_mirroring_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_mirroring_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'] + new_want = self.preprocess_want(want) + self.validate_want(new_want, state) + + commands = [] + requests = [] + if not new_want: + new_want = {} + + diff = get_diff(new_want, have, TEST_KEYS) + + if state == 'overridden': + commands, requests = self._state_overridden(new_want, have, diff) + elif state == 'replaced': + commands, requests = self._state_replaced(new_want, have, diff) + elif state == 'deleted': + commands, requests = self._state_deleted(new_want, have, diff) + elif state == 'merged': + commands, requests = self._state_merged(new_want, have, diff) + return commands, requests + + def _state_merged(self, want, have, diff): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + commands = [] + command = diff + requests = self.get_modify_mirroring_requests(command, have) + if command and len(requests) > 0: + commands = update_states([command], "merged") + else: + commands = [] + + return commands, requests + + def _state_deleted(self, want, have, diff): + """ The command generator when state is deleted + + :param want: the objects from which the configuration should be removed + :param obj_in_have: the current configuration as a dictionary + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + # if want is none, then delete all the mirroring except admin + commands = [] + global delete_all + delete_all = False + if not want: + command = have + delete_all = True + else: + command = want + + requests = self.get_delete_mirroring_requests(command, have, delete_all) + + if command and len(requests) > 0: + commands = update_states([command], "deleted") + + 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_mirroring_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_mirroring_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_mirroring_requests(have, have, True) + requests.extend(del_requests) + commands.extend(update_states(have, "deleted")) + have = [] + + if not have and want: + want_commands = want + want_requests = self.get_modify_mirroring_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_modify_span_requests(self, command): + requests = [] + + config = [] + span = command.get('span', []) + if span: + for ms in span: + name = ms.get('name') + dst_port = ms.get('dst_port') + source = ms.get('source') + direction = ms.get('direction') + + conf = {'name': name} + if dst_port: + conf['dst_port'] = dst_port + if source: + conf['src_port'] = source + if direction: + conf['direction'] = direction.upper() + + conf['type'] = 'span' + config.append(conf) + + path = URL + payload = {'MIRROR_SESSION_LIST': config} + request = {'path': path, 'method': PATCH, 'data': payload} + requests.append(request) + + return requests + + def get_modify_erspan_requests(self, command): + requests = [] + + config = [] + erspan = command.get('erspan', []) + if erspan: + for ms in erspan: + name = ms.get('name') + dst_ip = ms.get('dst_ip') + src_ip = ms.get('src_ip') + source = ms.get('source') + direction = ms.get('direction') + dscp = ms.get('dscp') + gre = ms.get('gre') + ttl = ms.get('ttl') + queue = ms.get('queue') + + conf = {'name': name} + if dst_ip: + conf['dst_ip'] = dst_ip + if src_ip: + conf['src_ip'] = src_ip + if source: + conf['src_port'] = source + if direction: + conf['direction'] = direction.upper() + if dscp is not None: + conf['dscp'] = dscp + if gre: + conf['gre_type'] = gre + if ttl is not None: + conf['ttl'] = ttl + if queue is not None: + conf['queue'] = queue + + conf['type'] = 'erspan' + config.append(conf) + + path = URL + payload = {'MIRROR_SESSION_LIST': config} + request = {'path': path, 'method': PATCH, 'data': payload} + requests.append(request) + + return requests + + def get_modify_mirroring_requests(self, command, have): + requests = [] + if not command: + return requests + + span_requests = self.get_modify_span_requests(command) + if span_requests: + requests.extend(span_requests) + + erspan_requests = self.get_modify_erspan_requests(command) + if erspan_requests: + requests.extend(erspan_requests) + + return requests + + def get_delete_mirroring_requests(self, command, have, is_delete_all=False): + requests = [] + if not command or not have: + return requests + + if is_delete_all: + path = URL + request = {'path': path, 'method': DELETE} + requests.append(request) + return requests + + c_span = command.get('span', []) + c_erspan = command.get('erspan', []) + h_span = have.get('span', []) + h_erspan = have.get('erspan', []) + + for ms in c_span: + ms_name = ms.get('name') + if next((h_ms for h_ms in h_span if h_ms['name'] == ms_name), None): + path = (URL + '={name}').format(name=ms_name) + request = {'path': path, 'method': DELETE} + requests.append(request) + for ms in c_erspan: + ms_name = ms.get('name') + if next((h_ms for h_ms in h_erspan if h_ms['name'] == ms_name), None): + path = (URL + '={name}').format(name=ms_name) + request = {'path': path, 'method': DELETE} + requests.append(request) + return requests + + def validate_want(self, want, state): + if not want: + return + + span = want.get('span', []) + erspan = want.get('erspan', []) + + if span and erspan: + for ms in span: + name = ms['name'] + in_erspan = next((ems for ems in erspan if name == ems['name']), None) + if in_erspan: + err_msg = "Names of SPAN and ERSPAN mirror sessions should not be duplicated." + self._module.fail_json(msg=err_msg, code=400) + + if state == 'deleted': + for ms in span: + if len(ms.keys()) > 1: + err_msg = "Attribute deletion of SPAN mirror session is not supported." + self._module.fail_json(msg=err_msg, code=400) + for ms in erspan: + if len(ms.keys()) > 1: + err_msg = "Attribute deletion of ERSPAN mirror session is not supported." + self._module.fail_json(msg=err_msg, code=400) + + def preprocess_want(self, want): + new_want = remove_empties(want) + if not new_want: + return new_want + + span = new_want.get('span', []) + erspan = new_want.get('erspan', []) + for ms in span: + dst_port = ms.get('dst_port') + if dst_port and dst_port != 'CPU': + ms['dst_port'] = get_normalize_interface_name(dst_port, self._module) + source = ms.get('source') + if source: + ms['source'] = get_normalize_interface_name(source, self._module) + + for ms in erspan: + source = ms.get('source') + if source: + ms['source'] = get_normalize_interface_name(source, self._module) + + return new_want + + def sort_mirrors(self, mirror_sessions): + if not mirror_sessions: + return + + span = mirror_sessions.get('span', []) + if span: + span.sort(key=lambda x: x['name']) + + erspan = mirror_sessions.get('erspan', []) + if erspan: + erspan.sort(key=lambda x: x['name']) diff --git a/plugins/module_utils/network/sonic/facts/facts.py b/plugins/module_utils/network/sonic/facts/facts.py index bb0dd3183..b63ff3d4e 100644 --- a/plugins/module_utils/network/sonic/facts/facts.py +++ b/plugins/module_utils/network/sonic/facts/facts.py @@ -73,6 +73,7 @@ from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.pim_interfaces.pim_interfaces import Pim_interfacesFacts from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.login_lockout.login_lockout import Login_lockoutFacts from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.poe.poe import PoeFacts +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.mirroring.mirroring import MirroringFacts FACT_LEGACY_SUBSETS = {} FACT_RESOURCE_SUBSETS = dict( @@ -130,7 +131,8 @@ pim_global=Pim_globalFacts, pim_interfaces=Pim_interfacesFacts, login_lockout=Login_lockoutFacts, - poe=PoeFacts + poe=PoeFacts, + mirroring=MirroringFacts ) diff --git a/plugins/module_utils/network/sonic/facts/mirroring/mirroring.py b/plugins/module_utils/network/sonic/facts/mirroring/mirroring.py new file mode 100644 index 000000000..a265bf183 --- /dev/null +++ b/plugins/module_utils/network/sonic/facts/mirroring/mirroring.py @@ -0,0 +1,147 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +The sonic mirroring 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.mirroring.mirroring import MirroringArgs +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 MirroringFacts(object): + """ The sonic mirroring fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = MirroringArgs.argument_spec + spec = deepcopy(self.argument_spec) + if subspec: + if options: + facts_argument_spec = spec[subspec][options] + else: + facts_argument_spec = spec[subspec] + else: + facts_argument_spec = spec + + self.generated_spec = utils.generate_dict(facts_argument_spec) + + def get_mirror_session_config(self): + """Get all mirroring sessions available in chassis""" + request = [{"path": "data/openconfig-mirror-ext:mirror/sessions", "method": GET}] + try: + response = edit_config(self._module, to_request(self._module, request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + if response[0][1]: + data = response[0][1]['openconfig-mirror-ext:sessions'] + else: + data = {} + + return data + + def populate_facts(self, connection, ansible_facts, data=None): + """ Populate the facts for mirroring + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + if not data: + data = self.get_mirror_session_config() + + if data and data.get('session'): + mirror_session_config = data['session'] + else: + mirror_session_config = [] + + mirror_session_facts = self.get_mirror_session_facts(mirror_session_config) + obj = self.render_config(self.generated_spec, mirror_session_facts) + + ansible_facts['ansible_network_resources'].pop('mirroring', None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {'config': obj}) + facts['mirroring'] = 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_mirror_session_facts(self, config): + mirror_session_facts = dict() + span = list() + erspan = list() + + for conf in config: + ms_info = conf.get('config') + if not ms_info: + continue + + name = ms_info.get('name') + dst_port = ms_info.get('dst-port') + source = ms_info.get('src-port') + dst_ip = ms_info.get('dst-ip') + src_ip = ms_info.get('src-ip') + direction = ms_info.get('direction') + if direction: + direction = direction.lower() + dscp = ms_info.get('dscp') + ttl = ms_info.get('ttl') + gre = ms_info.get('gre-type') + queue = ms_info.get('queue') + + is_erspan = False + for attr in [dst_ip, src_ip, dscp, ttl, gre, queue]: + if attr is not None: + is_erspan = True + break + if is_erspan: + erspan.append({'name': name, 'dst_ip': dst_ip, + 'src_ip': src_ip, 'source': source, + 'direction': direction, 'dscp': dscp, + 'ttl': ttl, 'gre': gre, 'queue': queue}) + else: + span.append({'name': name, 'dst_port': dst_port, + 'source': source, 'direction': direction}) + if span: + mirror_session_facts['span'] = remove_empties_from_list(span) + if erspan: + mirror_session_facts['erspan'] = remove_empties_from_list(erspan) + + return mirror_session_facts diff --git a/plugins/modules/sonic_facts.py b/plugins/modules/sonic_facts.py index a291b48b5..20c701ad2 100644 --- a/plugins/modules/sonic_facts.py +++ b/plugins/modules/sonic_facts.py @@ -107,6 +107,7 @@ - qos_interfaces - pim_global - pim_interfaces + - mirroring - login_lockout """ diff --git a/plugins/modules/sonic_mirroring.py b/plugins/modules/sonic_mirroring.py new file mode 100644 index 000000000..ad6315654 --- /dev/null +++ b/plugins/modules/sonic_mirroring.py @@ -0,0 +1,396 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# © Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for sonic_mirroring +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_mirroring +version_added: 3.0.0 +author: "M. Zhang (@mingjunzhang2019)" +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: port mirroring configuration handling for SONiC. +description: + - This module provides configuration management for mirroring on devices running SONiC. +options: + config: + description: + - Specifies port mirroring configuration. + type: dict + suboptions: + span: + description: + - SPAN mirroring sessions. + type: list + elements: dict + suboptions: + name: + description: + - SPAN mirroring session name. + required: true + type: str + dst_port: + description: + - Mirror session destination interface. + It may be CPU or an Ethernet interface. + type: str + source: + description: + - Mirror session source interface. + It may be an Ethernet interface or a PortChannel interface. + type: str + direction: + description: + - Mirror session direction. + type: str + choices: + - rx + - tx + - both + erspan: + description: + - ERSPAN mirroring sessions. + type: list + elements: dict + suboptions: + name: + description: + - ERSPAN mirroring session name. + required: true + type: str + dst_ip: + description: + - ERSPAN destination IP address. + type: str + src_ip: + description: + - ERSPAN source IP address. + type: str + source: + description: + - Mirror session source interface. + It may be an Ethernet interface or a PortChannel interface. + type: str + direction: + description: + - Mirror session direction. + type: str + choices: + - rx + - tx + - both + dscp: + description: + - ERSPAN destination DSCP. + The range of values is from 0 to 63. + type: int + gre: + description: + - ERSPAN destination GRE type. + A Hexadecimal string which must be quoted. + type: str + ttl: + description: + - ERSPAN destination TTL + The range of values is from 0 to 63. + type: int + queue: + description: + - ERSPAN destination queue number. + The range of values is from 0 to 63. + Only queue 0 is supported. + type: int + state: + description: + - Specifies the operation to be performed on the mirroring configured on the device. + In case of merged, the input configuration will be merged with the existing mirroring on the device. + In case of deleted, the existing mirroring configuration will be removed from the device. + In case of overridden, all existing mirroring will be deleted and the specified input configuration will be add. + In case of replaced, the existing mirroring on the device will be replaced by the configuration for + each mirroring. + type: str + choices: + - merged + - deleted + - replaced + - overridden + default: merged +""" +EXAMPLES = """ +# +# Using deleted +# +# Before state: +# ------------- +#sonic(config)# do show mirror-session +#ERSPAN Sessions +#----------------------------------------------------------------------------------------------------------------------------------------- +#Name Status SRC-IP DST-IP GRE DSCP TTL Queue Policer SRC-Port Direction +#----------------------------------------------------------------------------------------------------------------------------------------- +#dell-2 inactive 200.22.22.22 100.11.11.11 0 Ethernet28 both +#SPAN Sessions +#------------------------------------------------------------------------------- +#Name Status DST-Port SRC-Port Direction +#------------------------------------------------------------------------------- +#dell-1 active CPU Ethernet24 both +#dell-3 active CPU +# +- name: Merge some configuration + dellemc.enterprise_sonic.sonic_mirroring: + config: + span: + - name: dell-3 + erspan: + - name: dell-2 + state: deleted +# +# After state: +# ------------ +#sonic(config)# do show mirror-session +#SPAN Sessions +#------------------------------------------------------------------------------- +#Name Status DST-Port SRC-Port Direction +#------------------------------------------------------------------------------- +#dell-1 active CPU Ethernet24 both +# +# +# Using merged +# +# Before state: +# ------------- +# +#sonic(config)# do show mirror-session +#No sessions configured +#--------------------------------------------------------- +#RADIUS Global Configuration +#--------------------------------------------------------- +# +- name: Merge some configuration + dellemc.enterprise_sonic.sonic_mirroring: + config: + span: + - name: dell-1 + dst_port: CPU + source: Ethernet24 + direction: both + erspan: + - name: dell-2 + dst_ip: 100.11.11.11 + src_ip: 200.22.22.22 + source: Ethernet28 + direction: both + queue: 0 + state: merged +# +# After state: +# ------------ +#sonic(config)# do show mirror-session +#ERSPAN Sessions +#----------------------------------------------------------------------------------------------------------------------------------------- +#Name Status SRC-IP DST-IP GRE DSCP TTL Queue Policer SRC-Port Direction +#----------------------------------------------------------------------------------------------------------------------------------------- +#dell-2 inactive 200.22.22.22 100.11.11.11 0 Ethernet28 both +#SPAN Sessions +#------------------------------------------------------------------------------- +#Name Status DST-Port SRC-Port Direction +#------------------------------------------------------------------------------- +#dell-1 active CPU Ethernet24 both +# +# +# Using replaced +# +# Before state: +# ------------- +# +#sonic(config)# do show mirror-session +#ERSPAN Sessions +#----------------------------------------------------------------------------------------------------------------------------------------- +#Name Status SRC-IP DST-IP GRE DSCP TTL Queue Policer SRC-Port Direction +#----------------------------------------------------------------------------------------------------------------------------------------- +#dell-2 inactive 200.22.22.22 100.11.11.11 0 Ethernet28 both +#SPAN Sessions +#------------------------------------------------------------------------------- +#Name Status DST-Port SRC-Port Direction +#------------------------------------------------------------------------------- +#dell-1 active CPU Ethernet24 both +#dell-3 active CPU +# +- name: Replace some configuration + dellemc.enterprise_sonic.sonic_mirroring: + config: + erspan: + - name: dell-2 + dst_ip: 32.22.22.12 + src_ip: 31.21.21.12 + source: Ethernet28 + dscp: 6 + gre: "0x6689" + ttl: 9 + queue: 0 + direction: rx + - name: dell-3 + dst_ip: 22.22.22.12 + src_ip: 21.21.21.12 + source: Ethernet28 + direction: rx + span: + - name: dell-1 + dst_port: Ethernet4 + source: Ethernet24 + direction: tx + state: replaced +# +# After state: +# ------------ +#sonic(config)# do show mirror-session +#ERSPAN Sessions +#----------------------------------------------------------------------------------------------------------------------------------------- +#Name Status SRC-IP DST-IP GRE DSCP TTL Queue Policer SRC-Port Direction +#----------------------------------------------------------------------------------------------------------------------------------------- +#dell-2 inactive 32.22.22.22 31.11.11.11 0x6689 6 9 0 Ethernet28 rx +#dell-3 inactive 21.21.21.12 22.22.22.12 Ethernet28 rx +#SPAN Sessions +#------------------------------------------------------------------------------- +#Name Status DST-Port SRC-Port Direction +#------------------------------------------------------------------------------- +#dell-1 active Ethertnet4 Ethernet24 tx +#dell-3 active CPU +# +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic(config)# do show mirror-session +#ERSPAN Sessions +#----------------------------------------------------------------------------------------------------------------------------------------- +#Name Status SRC-IP DST-IP GRE DSCP TTL Queue Policer SRC-Port Direction +#----------------------------------------------------------------------------------------------------------------------------------------- +#dell-2 inactive 200.22.22.22 100.11.11.11 0 Ethernet28 both +#SPAN Sessions +#------------------------------------------------------------------------------- +#Name Status DST-Port SRC-Port Direction +#------------------------------------------------------------------------------- +#dell-1 active CPU Ethernet24 both +#dell-3 active CPU +# +- name: Override mirrot session configuration + dellemc.enterprise_sonic.sonic_mirroring: + config: + erspan: + - name: dell-2 + dst_ip: 32.22.22.12 + src_ip: 31.21.21.12 + source: Ethernet28 + gre: "0x6689" + dscp: 6 + ttl: 9 + queue: 0 + direction: rx + - name: dell-1 + dst_ip: 22.22.22.12 + src_ip: 21.21.21.12 + source: Ethernet28 + direction: rx + span: + - name: dell-6 + dst_port: CPU + source: Ethernet24 + direction: tx + state: overridden +# +# After state: +# ------------ +#sonic(config)# do show mirror-session +#ERSPAN Sessions +#----------------------------------------------------------------------------------------------------------------------------------------- +#Name Status SRC-IP DST-IP GRE DSCP TTL Queue Policer SRC-Port Direction +#----------------------------------------------------------------------------------------------------------------------------------------- +#dell-1 inactive 21.21.21.12 22.22.22.12 Ethernet28 rx +#dell-2 inactive 31.21.21.12 32.22.22.12 0x6689 6 9 0 Ethernet28 rx +#SPAN Sessions +#------------------------------------------------------------------------------- +#Name Status DST-Port SRC-Port Direction +#------------------------------------------------------------------------------- +#dell-6 active Ethertnet4 Ethernet24 tx +# +""" +RETURN = """ +before: + description: The configuration prior to the module 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 module invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + as the parameters above. +after(generated): + description: The generated configuration module invocation. + returned: when C(check_mode) + 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.mirroring.mirroring import MirroringArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.mirroring.mirroring import Mirroring + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=MirroringArgs.argument_spec, + supports_check_mode=True) + + result = Mirroring(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/regression/roles/sonic_mirroring/defaults/main.yml b/tests/regression/roles/sonic_mirroring/defaults/main.yml new file mode 100644 index 000000000..fdfcd3af0 --- /dev/null +++ b/tests/regression/roles/sonic_mirroring/defaults/main.yml @@ -0,0 +1,110 @@ +--- +ansible_connection: httpapi +module_name: mirroring + +po1: Portchannel 100 +po2: Portchannel 200 + +preparations_tests: + delete_interfaces: + - "no interface {{ po1 }}" + - "no interface {{ po2 }}" + init_interfaces: + - "interface {{ po1 }}" + - "interface {{ po2 }}" + +tests: + - name: test_case_01 + description: Configure some mirror sessions + state: merged + input: + span: + - name: dell-1 + dst_port: CPU + source: "{{ po1 }}" + direction: both + - name: dell-2 + dst_port: "{{ interface1 }}" + source: "{{ interface10 }}" + direction: tx + erspan: + - name: dell-3 + dst_ip: 100.11.11.11 + src_ip: 200.22.22.22 + source: "{{ po2 }}" + direction: both + dscp: 6 + gre: "0x6689" + ttl: 9 + queue: 0 + + - name: test_case_02 + description: Merge some mirror session configuration + state: merged + input: + span: + - name: dell-2 + dst_port: CPU + source: "{{ interface11 }}" + direction: both + erspan: + - name: dell-4 + dst_ip: 100.33.33.33 + src_ip: 200.55.55.55 + source: "{{ interface12 }}" + direction: both + + - name: test_case_03 + description: Replace some mirror sessions + state: replaced + input: + span: + - name: dell-1 + dst_port: "{{ interface5 }}" + source: "{{ interface6 }}" + direction: both + - name: dell-5 + dst_port: "{{ interface2 }}" + source: "{{ po1 }}" + direction: tx + erspan: + - name: dell-3 + dst_ip: 100.11.11.11 + src_ip: 200.22.22.22 + source: "{{ po2 }}" + direction: both + + - name: test_case_04 + description: Delete some mirror sessions + state: deleted + input: + span: + - name: dell-1 + - name: dell-5 + erspan: + - name: dell-4 + + - name: test_case_5 + description: Overridden mirror session configuration + state: overridden + input: + span: + - name: dell-1 + dst_port: "{{ interface5 }}" + source: "{{ po1 }}" + direction: rx + - name: dell-5 + dst_port: "{{ interface2 }}" + source: "{{ po1 }}" + direction: tx + erspan: + - name: dell-6 + dst_ip: 100.66.66.66 + src_ip: 200.66.66.66 + source: "{{ interface7 }}" + direction: both + + - name: test_case_6 + description: Delete all mirror sessions + state: deleted + input: {} diff --git a/tests/regression/roles/sonic_mirroring/meta/main.yaml b/tests/regression/roles/sonic_mirroring/meta/main.yaml new file mode 100644 index 000000000..611fd54d2 --- /dev/null +++ b/tests/regression/roles/sonic_mirroring/meta/main.yaml @@ -0,0 +1,5 @@ +--- +collections: + - dellemc.enterprise_sonic +dependencies: + - { role: common } \ No newline at end of file diff --git a/tests/regression/roles/sonic_mirroring/tasks/cleanup_tests.yaml b/tests/regression/roles/sonic_mirroring/tasks/cleanup_tests.yaml new file mode 100644 index 000000000..fc33c374f --- /dev/null +++ b/tests/regression/roles/sonic_mirroring/tasks/cleanup_tests.yaml @@ -0,0 +1,7 @@ +- name: Delete interfaces + vars: + ansible_connection: network_cli + sonic_config: + commands: "{{ preparations_tests.delete_interfaces }}" + register: output + ignore_errors: yes diff --git a/tests/regression/roles/sonic_mirroring/tasks/main.yml b/tests/regression/roles/sonic_mirroring/tasks/main.yml new file mode 100644 index 000000000..dd105dd84 --- /dev/null +++ b/tests/regression/roles/sonic_mirroring/tasks/main.yml @@ -0,0 +1,11 @@ +- debug: msg="sonic_mirroring Test started ..." + +- name: Preparations tests + include_tasks: preparation_tests.yaml + +- name: "Test {{ module_name }} started ..." + include_tasks: tasks_template.yaml + loop: "{{ tests }}" + +- name: "Cleanup test {{ module_name }} started" + include_tasks: cleanup_tests.yaml diff --git a/tests/regression/roles/sonic_mirroring/tasks/preparation_tests.yaml b/tests/regression/roles/sonic_mirroring/tasks/preparation_tests.yaml new file mode 100644 index 000000000..146c6ba26 --- /dev/null +++ b/tests/regression/roles/sonic_mirroring/tasks/preparation_tests.yaml @@ -0,0 +1,13 @@ +- name: Delete existing NTP configurations + sonic_mirroring: + config: {} + state: deleted + ignore_errors: yes + +- name: Initialize interfaces + vars: + ansible_connection: network_cli + sonic_config: + commands: "{{ preparations_tests.init_interfaces }}" + register: output + ignore_errors: yes diff --git a/tests/regression/roles/sonic_mirroring/tasks/tasks_template.yaml b/tests/regression/roles/sonic_mirroring/tasks/tasks_template.yaml new file mode 100644 index 000000000..7d35e4047 --- /dev/null +++ b/tests/regression/roles/sonic_mirroring/tasks/tasks_template.yaml @@ -0,0 +1,21 @@ +- name: "{{ item.name}} , {{ item.description}}" + sonic_mirroring: + config: "{{ item.input }}" + state: "{{ item.state }}" + register: action_task_output + ignore_errors: yes + +- import_role: + name: common + tasks_from: action.facts.report.yaml + +- name: "{{ item.name}} , {{ item.description}} Idempotent" + sonic_mirroring: + config: "{{ item.input }}" + state: "{{ item.state }}" + register: idempotent_task_output + ignore_errors: yes + +- import_role: + name: common + tasks_from: idempotent.facts.report.yaml diff --git a/tests/regression/test.yaml b/tests/regression/test.yaml index 05d018557..2fccd5574 100644 --- a/tests/regression/test.yaml +++ b/tests/regression/test.yaml @@ -66,4 +66,5 @@ - sonic_pim_interfaces - sonic_login_lockout - sonic_poe + - sonic_mirroring - test_reports diff --git a/tests/unit/modules/network/sonic/fixtures/sonic_mirroring.yaml b/tests/unit/modules/network/sonic/fixtures/sonic_mirroring.yaml new file mode 100644 index 000000000..2e77d5d79 --- /dev/null +++ b/tests/unit/modules/network/sonic/fixtures/sonic_mirroring.yaml @@ -0,0 +1,271 @@ +--- +merged_01: + module_args: + config: + span: + - name: dell-1 + dst_port: CPU + source: Eth1/5 + direction: both + - name: dell-2 + dst_port: CPU + erspan: + - name: dell-3 + dst_ip: 11.11.11.12 + src_ip: 21.21.21.12 + source: Eth1/6 + direction: tx + existing_mirroring_config: + - path: "data/openconfig-mirror-ext:mirror/sessions" + response: + code: 200 + expected_config_requests: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "patch" + data: + MIRROR_SESSION_LIST: + - name: dell-1 + dst_port: CPU + src_port: Eth1/5 + direction: BOTH + type: span + - name: dell-2 + dst_port: CPU + type: span + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "patch" + data: + MIRROR_SESSION_LIST: + - name: dell-3 + dst_ip: 11.11.11.12 + src_ip: 21.21.21.12 + src_port: Eth1/6 + direction: TX + type: erspan + +deleted_01: + module_args: + state: deleted + existing_mirroring_config: + - path: "data/openconfig-mirror-ext:mirror/sessions" + response: + code: 200 + value: + openconfig-mirror-ext:sessions: + session: + - name: dell-1 + config: + name: dell-1 + direction: both + dst-port: CPU + src-port: Eth1/5 + - name: dell-2 + config: + name: dell-2 + direction: both + dst-ip: 100.11.11.11 + src-ip: 200.22.22.22 + src-port: Eth1/6 + queue: 0 + expected_config_requests: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "delete" + data: + +deleted_02: + module_args: + state: deleted + config: + span: + - name: dell-1 + erspan: + - name: dell-2 + existing_mirroring_config: + - path: "data/openconfig-mirror-ext:mirror/sessions" + response: + code: 200 + value: + openconfig-mirror-ext:sessions: + session: + - name: dell-1 + config: + name: dell-1 + direction: both + dst-port: CPU + src-port: Eth1/4 + - name: dell-2 + config: + name: dell-2 + direction: both + dst-ip: 100.11.11.11 + src-ip: 200.22.22.22 + src-port: Eth1/6 + queue: 0 + - name: dell-3 + config: + name: dell-3 + direction: tx + dst-port: Eth1/1 + src-port: Eth1/5 + expected_config_requests: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST=dell-1" + method: "delete" + data: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST=dell-2" + method: "delete" + data: + +replaced_01: + module_args: + state: replaced + config: + erspan: + - name: dell-2 + dst_ip: 32.22.22.12 + src_ip: 31.21.21.12 + source: Eth1/6 + dscp: 6 + gre: "0x6689" + ttl: 9 + queue: 0 + direction: rx + span: + - name: dell-1 + dst_port: Eth1/1 + source: Eth1/4 + direction: rx + - name: dell-3 + dst_port: CPU + source: Eth1/4 + direction: tx + existing_mirroring_config: + - path: "data/openconfig-mirror-ext:mirror/sessions" + response: + code: 200 + value: + openconfig-mirror-ext:sessions: + session: + - name: dell-1 + config: + name: dell-1 + direction: both + dst-port: CPU + src-port: Eth1/4 + - name: dell-2 + config: + name: dell-2 + direction: both + dst-ip: 100.11.11.11 + src-ip: 200.22.22.22 + src-port: Eth1/6 + queue: 0 + expected_config_requests: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST=dell-1" + method: "delete" + data: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST=dell-2" + method: "delete" + data: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "patch" + data: + MIRROR_SESSION_LIST: + - name: dell-1 + dst_port: Eth1/1 + src_port: Eth1/4 + direction: RX + type: span + - name: dell-3 + dst_port: CPU + src_port: Eth1/4 + direction: TX + type: span + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "patch" + data: + MIRROR_SESSION_LIST: + - name: dell-2 + dst_ip: 32.22.22.12 + src_ip: 31.21.21.12 + src_port: Eth1/6 + direction: RX + dscp: 6 + gre_type: "0x6689" + ttl: 9 + queue: 0 + type: erspan + +overridden_01: + module_args: + state: overridden + config: + erspan: + - name: dell-2 + dst_ip: 32.22.22.12 + src_ip: 31.21.21.12 + source: Eth1/6 + gre: "0x6689" + queue: 0 + direction: rx + - name: dell-6 + dst_ip: 22.22.22.12 + src_ip: 21.21.21.12 + source: Eth1/6 + direction: rx + span: + - name: dell-3 + dst_port: CPU + source: Eth1/4 + direction: tx + existing_mirroring_config: + - path: "data/openconfig-mirror-ext:mirror/sessions" + response: + code: 200 + value: + openconfig-mirror-ext:sessions: + session: + - name: dell-1 + config: + name: dell-1 + direction: both + dst-port: CPU + src-port: Eth1/4 + - name: dell-2 + config: + name: dell-2 + direction: both + dst-ip: 100.11.11.11 + src-ip: 200.22.22.22 + src-port: Eth1/6 + queue: 0 + expected_config_requests: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "delete" + data: + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "patch" + data: + MIRROR_SESSION_LIST: + - name: dell-3 + dst_port: CPU + src_port: Eth1/4 + direction: TX + type: span + - path: "data/sonic-mirror-session:sonic-mirror-session/MIRROR_SESSION/MIRROR_SESSION_LIST" + method: "patch" + data: + MIRROR_SESSION_LIST: + - name: dell-2 + dst_ip: 32.22.22.12 + src_ip: 31.21.21.12 + src_port: Eth1/6 + direction: RX + gre_type: "0x6689" + queue: 0 + type: erspan + - name: dell-6 + dst_ip: 22.22.22.12 + src_ip: 21.21.21.12 + src_port: Eth1/6 + direction: RX + type: erspan diff --git a/tests/unit/modules/network/sonic/test_sonic_mirroring.py b/tests/unit/modules/network/sonic/test_sonic_mirroring.py new file mode 100644 index 000000000..e12f00362 --- /dev/null +++ b/tests/unit/modules/network/sonic/test_sonic_mirroring.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.dellemc.enterprise_sonic.tests.unit.compat.mock import ( + patch, +) +from ansible_collections.dellemc.enterprise_sonic.plugins.modules import ( + sonic_mirroring, +) +from ansible_collections.dellemc.enterprise_sonic.tests.unit.modules.utils import ( + set_module_args, +) +from .sonic_module import TestSonicModule + + +class TestSonicMirroringModule(TestSonicModule): + module = sonic_mirroring + + @classmethod + def setUpClass(cls): + cls.mock_facts_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.mirroring.mirroring.edit_config" + ) + cls.mock_config_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.mirroring.mirroring.edit_config" + ) + cls.mock_get_interface_naming_mode = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils.get_device_interface_naming_mode" + ) + cls.fixture_data = cls.load_fixtures('sonic_mirroring.yaml') + + def setUp(self): + super(TestSonicMirroringModule, self).setUp() + self.facts_edit_config = self.mock_facts_edit_config.start() + self.config_edit_config = self.mock_config_edit_config.start() + + self.facts_edit_config.side_effect = self.facts_side_effect + self.config_edit_config.side_effect = self.config_side_effect + + self.get_interface_naming_mode = self.mock_get_interface_naming_mode.start() + self.get_interface_naming_mode.return_value = 'standard' + + def tearDown(self): + super(TestSonicMirroringModule, self).tearDown() + self.mock_facts_edit_config.stop() + self.mock_config_edit_config.stop() + self.mock_get_interface_naming_mode.stop() + + def test_sonic_mirroring_merged_01(self): + set_module_args(self.fixture_data['merged_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['merged_01']['existing_mirroring_config']) + self.initialize_config_requests(self.fixture_data['merged_01']['expected_config_requests']) + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_mirroring_deleted_01(self): + set_module_args(self.fixture_data['deleted_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_01']['existing_mirroring_config']) + self.initialize_config_requests(self.fixture_data['deleted_01']['expected_config_requests']) + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_mirroring_deleted_02(self): + set_module_args(self.fixture_data['deleted_02']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_02']['existing_mirroring_config']) + self.initialize_config_requests(self.fixture_data['deleted_02']['expected_config_requests']) + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_mirroring_replaced_01(self): + set_module_args(self.fixture_data['replaced_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['replaced_01']['existing_mirroring_config']) + self.initialize_config_requests(self.fixture_data['replaced_01']['expected_config_requests']) + result = self.execute_module(changed=True) + self.validate_config_requests() + + def test_sonic_mirroring_overridden_01(self): + set_module_args(self.fixture_data['overridden_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['overridden_01']['existing_mirroring_config']) + self.initialize_config_requests(self.fixture_data['overridden_01']['expected_config_requests']) + result = self.execute_module(changed=True) + self.validate_config_requests()