From 6b899c616a3cf55e4c8687f063ce5dd624c3f925 Mon Sep 17 00:00:00 2001 From: mingjunzhang2019 Date: Thu, 22 Aug 2024 16:11:12 -0500 Subject: [PATCH 1/3] Add IP ECMP loadshare mode resource module --- .../network/sonic/argspec/facts/facts.py | 3 +- .../argspec/loadshare_mode/loadshare_mode.py | 94 ++++ .../config/loadshare_mode/loadshare_mode.py | 448 ++++++++++++++++++ .../module_utils/network/sonic/facts/facts.py | 4 +- .../facts/loadshare_mode/loadshare_mode.py | 253 ++++++++++ plugins/modules/sonic_facts.py | 1 + plugins/modules/sonic_loadshare_mode.py | 376 +++++++++++++++ .../sonic_loadshare_mode/defaults/main.yml | 90 ++++ .../roles/sonic_loadshare_mode/meta/main.yaml | 5 + .../roles/sonic_loadshare_mode/tasks/main.yml | 8 + .../tasks/preparation_tests.yaml | 5 + .../tasks/tasks_template.yaml | 21 + tests/regression/test.yaml | 1 + .../sonic/fixtures/sonic_loadshare_mode.yaml | 323 +++++++++++++ .../sonic/test_sonic_loadshare_mode.py | 83 ++++ 15 files changed, 1713 insertions(+), 2 deletions(-) create mode 100644 plugins/module_utils/network/sonic/argspec/loadshare_mode/loadshare_mode.py create mode 100644 plugins/module_utils/network/sonic/config/loadshare_mode/loadshare_mode.py create mode 100644 plugins/module_utils/network/sonic/facts/loadshare_mode/loadshare_mode.py create mode 100644 plugins/modules/sonic_loadshare_mode.py create mode 100644 tests/regression/roles/sonic_loadshare_mode/defaults/main.yml create mode 100644 tests/regression/roles/sonic_loadshare_mode/meta/main.yaml create mode 100644 tests/regression/roles/sonic_loadshare_mode/tasks/main.yml create mode 100644 tests/regression/roles/sonic_loadshare_mode/tasks/preparation_tests.yaml create mode 100644 tests/regression/roles/sonic_loadshare_mode/tasks/tasks_template.yaml create mode 100644 tests/unit/modules/network/sonic/fixtures/sonic_loadshare_mode.yaml create mode 100644 tests/unit/modules/network/sonic/test_sonic_loadshare_mode.py diff --git a/plugins/module_utils/network/sonic/argspec/facts/facts.py b/plugins/module_utils/network/sonic/argspec/facts/facts.py index be039db7c..c7ed23e6f 100644 --- a/plugins/module_utils/network/sonic/argspec/facts/facts.py +++ b/plugins/module_utils/network/sonic/argspec/facts/facts.py @@ -77,7 +77,8 @@ def __init__(self, **kwargs): 'pim_global', 'pim_interfaces', 'login_lockout', - 'poe' + 'poe', + 'loadshare_mode' ] argument_spec = { diff --git a/plugins/module_utils/network/sonic/argspec/loadshare_mode/loadshare_mode.py b/plugins/module_utils/network/sonic/argspec/loadshare_mode/loadshare_mode.py new file mode 100644 index 000000000..fc3714425 --- /dev/null +++ b/plugins/module_utils/network/sonic/argspec/loadshare_mode/loadshare_mode.py @@ -0,0 +1,94 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2024 Dell Inc. or its subsidiaries. All Rights Reserved +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# 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_loadshare_mode module +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class Loadshare_modeArgs(object): # pylint: disable=R0903 + + """The arg spec for the sonic_loadshare_mode module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + "config": { + "options": { + "hash_algorithm": { + "type": "str", + "choices": ["CRC", + "XOR", + "CRC_32LO", + "CRC_32HI", + "CRC_CCITT", + "CRC_XOR", + "JENKINS_HASH_LO", + "JENKINS_HASH_HI"]}, + "hash_ingress_port": {"type": "bool"}, + "hash_offset": { + "options": { + "offset": {"type": "int"}, + "flow_based": {"type": "bool"} + }, + "type": "dict" + }, + "hash_roce_qpn": {"type": "bool"}, + "hash_seed": {"type": "int"}, + "ipv4": { + "options": { + "ipv4_dst_ip": {"type": "bool"}, + "ipv4_src_ip": {"type": "bool"}, + "ipv4_ip_proto": {"type": "bool"}, + "ipv4_l4_dst_port": {"type": "bool"}, + "ipv4_l4_src_port": {"type": "bool"}, + "ipv4_symmetric": {"type": "bool"} + }, + "type": "dict" + }, + "ipv6": { + "options": { + "ipv6_dst_ip": {"type": "bool"}, + "ipv6_src_ip": {"type": "bool"}, + "ipv6_next_hdr": {"type": "bool"}, + "ipv6_l4_dst_port": {"type": "bool"}, + "ipv6_l4_src_port": {"type": "bool"}, + "ipv6_symmetric": {"type": "bool"} + }, + "type": "dict" + } + }, + "type": "dict" + }, + "state": { + "choices": ["merged", "replaced", "overridden", "deleted"], + "default": "merged", + "type": "str" + } + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/sonic/config/loadshare_mode/loadshare_mode.py b/plugins/module_utils/network/sonic/config/loadshare_mode/loadshare_mode.py new file mode 100644 index 000000000..80bb18d47 --- /dev/null +++ b/plugins/module_utils/network/sonic/config/loadshare_mode/loadshare_mode.py @@ -0,0 +1,448 @@ +# +# -*- 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_loadshare_mode +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 +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.formatted_diff_utils import ( + __DELETE_LEAFS_THEN_CONFIG_IF_NO_NON_KEY_LEAF, + get_new_config, + get_formatted_config_diff +) +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.loadshare_mode.loadshare_mode import ( + LOADSHARE_MODE_ATTR_MAP, + LOADSHARE_MODE_DICT_MAP +) + +PATCH = 'patch' +DELETE = 'delete' +URL = 'data/openconfig-loadshare-mode-ext:loadshare' + + +delete_all = False +TEST_KEYS_generate_config = [ + {'config': {'__delete_op': __DELETE_LEAFS_THEN_CONFIG_IF_NO_NON_KEY_LEAF}}, + {'ipv4': {'__delete_op': __DELETE_LEAFS_THEN_CONFIG_IF_NO_NON_KEY_LEAF}}, + {'ipv6': {'__delete_op': __DELETE_LEAFS_THEN_CONFIG_IF_NO_NON_KEY_LEAF}}, + {'hash_offset': {'__delete_op': __DELETE_LEAFS_THEN_CONFIG_IF_NO_NON_KEY_LEAF}} +] + + +class Loadshare_mode(ConfigBase): + """ + The sonic_loadshare_mode class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'loadshare_mode', + ] + + def __init__(self, module): + super(Loadshare_mode, self).__init__(module) + + def get_loadshare_mode_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) + loadshare_mode_facts = facts['ansible_network_resources'].get('loadshare_mode') + if not loadshare_mode_facts: + return {} + return loadshare_mode_facts + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + existing_loadshare_mode_facts = self.get_loadshare_mode_facts() + commands, requests = self.set_config(existing_loadshare_mode_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_loadshare_mode_facts = self.get_loadshare_mode_facts() + + result['before'] = existing_loadshare_mode_facts + if result['changed']: + result['after'] = changed_loadshare_mode_facts + + new_config = changed_loadshare_mode_facts + old_config = existing_loadshare_mode_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: + 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'] + + commands = [] + requests = [] + + want = remove_empties(want) + if state == 'overridden': + commands, requests = self._state_overridden(want, have) + elif state == 'replaced': + commands, requests = self._state_replaced(want, have) + elif state == 'deleted': + commands, requests = self._state_deleted(want, have) + elif state == 'merged': + commands, requests = self._state_merged(want, have) + return commands, requests + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :param want: the additive configuration as a dictionary + :param 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 = get_diff(want, have) + command = diff + requests = self.get_modify_loadshare_mode_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): + """ 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_loadshare_mode_requests(command, have, True, delete_all) + + if command and len(requests) > 0: + commands = update_states([command], "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 + """ + commands = [] + requests = [] + self.preprocess_want(want, have) + diff = get_diff(want, have) + replaced_config = get_replaced_config(want, have) + + add_commands = [] + if replaced_config: + del_requests = self.get_delete_loadshare_mode_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_loadshare_mode_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 + """ + commands = [] + requests = [] + + self.preprocess_want(want, have) + diff = get_diff(want, have) + r_diff = get_diff(have, want) + if have and (diff or r_diff): + del_requests = self.get_delete_loadshare_mode_requests(have, have, False, 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_loadshare_mode_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_loadshare_mode_requests(self, command, have): + requests = [] + if not command: + return requests + + lsm_payload = dict() + + for amap in LOADSHARE_MODE_ATTR_MAP: + ans_attr = amap['ans_attr'] + cfg_attr = amap['cfg_attr'] + cfg_subattr = amap['cfg_subattr'] + cfg_hash = amap.get('hash') + + cmd_attr = command.get(ans_attr, None) + if cmd_attr is not None: + lsm_payload[cfg_attr] = {'config': {cfg_subattr: cmd_attr}} + if cfg_hash: + lsm_payload[cfg_attr]['config'].update(cfg_hash) + + for amap in LOADSHARE_MODE_DICT_MAP: + ans_attr = amap['ans_attr'] + cfg_attr = amap['cfg_attr'] + cfg_hash = amap.get('hash') + + cmd_attrs = command.get(ans_attr, None) + if cmd_attrs is not None: + amap_subattrs = amap['map_subattrs'] + config = {} + for amap_subattr in amap_subattrs: + ans_subattr = amap_subattr['ans_attr'] + cfg_subattr = amap_subattr['cfg_attr'] + cmd_subattr = cmd_attrs.get(ans_subattr, None) + if cmd_subattr is not None: + config.update({cfg_subattr: cmd_subattr}) + + if config: + lsm_payload[cfg_attr] = {'config': config} + if cfg_hash: + lsm_payload[cfg_attr]['config'].update(cfg_hash) + + if lsm_payload: + path = URL + payload = {"openconfig-loadshare-mode-ext:loadshare": lsm_payload} + request = {'path': path, 'method': PATCH, 'data': payload} + requests.append(request) + + return requests + + def check_delete(self, cmd_attr, have_attr, ans_delete): + # cmd_attr must not be None. + del_attr = False + if ans_delete: + if isinstance(cmd_attr, bool): + del_attr = cmd_attr and have_attr + elif isinstance(cmd_attr, int) or isinstance(cmd_attr, str): + del_attr = cmd_attr == have_attr + else: + if have_attr is not None: + del_attr = True + return del_attr + + def get_delete_loadshare_mode_requests(self, command, have, ans_delete=False, 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) + + if not ans_delete: + request = self.get_patch_config_with_defaults_request(have) + if request: + requests.append(request) + + return requests + + for amap in LOADSHARE_MODE_ATTR_MAP: + ans_attr = amap['ans_attr'] + cfg_attr = amap['cfg_attr'] + + cmd_attr = command.get(ans_attr, None) + have_attr = have.get(ans_attr, None) + + if cmd_attr is not None: + del_attr = self.check_delete(cmd_attr, have_attr, ans_delete) + if del_attr: + path = URL + '/' + cfg_attr + request = {'path': path, 'method': DELETE} + requests.append(request) + + lsm_payload = dict() + for amap in LOADSHARE_MODE_DICT_MAP: + ans_attr = amap['ans_attr'] + cfg_attr = amap['cfg_attr'] + cfg_hash = amap.get('hash') + del_op = amap['delete_op'] + + cmd_attrs = command.get(ans_attr, {}) + have_attrs = have.get(ans_attr, {}) + if cmd_attrs: + amap_subattrs = amap['map_subattrs'] + config = {} + for amap_subattr in amap_subattrs: + ans_subattr = amap_subattr['ans_attr'] + cfg_subattr = amap_subattr['cfg_attr'] + cmd_subattr = cmd_attrs.get(ans_subattr, None) + have_subattr = have_attrs.get(ans_subattr, None) + if cmd_subattr is not None: + del_attr = self.check_delete(cmd_subattr, have_subattr, ans_delete) + if del_attr: + if del_op == 'DELETE': + path = URL + '/' + cfg_attr + '/config/' + cfg_subattr + request = {'path': path, 'method': DELETE} + requests.append(request) + else: + config.update({cfg_subattr: False}) + + if config: + lsm_payload[cfg_attr] = {'config': config} + if cfg_hash: + lsm_payload[cfg_attr]['config'].update(cfg_hash) + + if lsm_payload: + path = URL + payload = {"openconfig-loadshare-mode-ext:loadshare": lsm_payload} + request = {'path': path, 'method': PATCH, 'data': payload} + requests.append(request) + + return requests + + def get_patch_config_with_defaults_request(self, have): + + request = dict() + lsm_payload = dict() + + for attr in ['ipv4', 'ipv6']: + have_attr = have.get(attr) + if have_attr: + config = {} + attr_map = next((amap for amap in LOADSHARE_MODE_DICT_MAP if amap['ans_attr'] == attr), None) + cfg_attr = attr_map['cfg_attr'] + cfg_hash = attr_map.get('hash') + for amap_subattr in attr_map['map_subattrs']: + cfg_subattr = amap_subattr['cfg_attr'] + attr_dft_value = amap_subattr['dft_value'] + config.update({cfg_subattr: attr_dft_value}) + + config.update(cfg_hash) + lsm_payload[cfg_attr] = {'config': config} + + if lsm_payload: + path = URL + payload = {"openconfig-loadshare-mode-ext:loadshare": lsm_payload} + request = {'path': path, 'method': PATCH, 'data': payload} + + return request + + def preprocess_want(self, want, have): + + for attr in ['ipv4', 'ipv6']: + cmd_attr = want.get(attr) + have_attr = have.get(attr) + if cmd_attr and have_attr: + attr_map = next((amap for amap in LOADSHARE_MODE_DICT_MAP if amap['ans_attr'] == attr), None) + for amap_subattr in attr_map['map_subattrs']: + ans_subattr = amap_subattr['ans_attr'] + attr_dft_value = amap_subattr['dft_value'] + if not cmd_attr.get(ans_subattr) and have_attr.get(ans_subattr) == attr_dft_value: + cmd_attr[ans_subattr] = attr_dft_value diff --git a/plugins/module_utils/network/sonic/facts/facts.py b/plugins/module_utils/network/sonic/facts/facts.py index b6c5eb7bb..c8744e407 100644 --- a/plugins/module_utils/network/sonic/facts/facts.py +++ b/plugins/module_utils/network/sonic/facts/facts.py @@ -75,6 +75,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.loadshare_mode.loadshare_mode import Loadshare_modeFacts FACT_LEGACY_SUBSETS = {} FACT_RESOURCE_SUBSETS = dict( @@ -134,7 +135,8 @@ pim_global=Pim_globalFacts, pim_interfaces=Pim_interfacesFacts, login_lockout=Login_lockoutFacts, - poe=PoeFacts + poe=PoeFacts, + loadshare_mode=Loadshare_modeFacts ) diff --git a/plugins/module_utils/network/sonic/facts/loadshare_mode/loadshare_mode.py b/plugins/module_utils/network/sonic/facts/loadshare_mode/loadshare_mode.py new file mode 100644 index 000000000..6003053d5 --- /dev/null +++ b/plugins/module_utils/network/sonic/facts/loadshare_mode/loadshare_mode.py @@ -0,0 +1,253 @@ +# +# -*- 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 IP ECMP load share mode 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.loadshare_mode.loadshare_mode import Loadshare_modeArgs +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" + +LOADSHARE_MODE_ATTR_MAP = [ + { + 'ans_attr': 'hash_algorithm', + 'cfg_attr': 'hash-algorithm', + 'cfg_subattr': 'algorithm' + }, + { + 'hash': {'hash': 'hash'}, + 'ans_attr': 'hash_ingress_port', + 'cfg_attr': 'ingress-port', + 'cfg_subattr': 'ingress-port' + }, + { + 'hash': {'hash': 'hash'}, + 'ans_attr': 'hash_roce_qpn', + 'cfg_attr': 'roce-attrs', + 'cfg_subattr': 'qpn' + }, + { + 'hash': {'hash': 'hash'}, + 'ans_attr': 'hash_seed', + 'cfg_attr': 'seed-attrs', + 'cfg_subattr': 'ecmp-hash-seed' + } +] + +LOADSHARE_MODE_DICT_MAP = [ + { + 'hash': {'hash': 'hash'}, + 'ans_attr': 'hash_offset', + 'cfg_attr': 'offset-attrs', + 'delete_op': 'DELETE', + 'map_subattrs': [ + { + 'ans_attr': 'offset', + 'cfg_attr': 'ecmp-hash-offset' + }, + { + 'ans_attr': 'flow_based', + 'cfg_attr': 'ecmp-hash-flow-based' + } + ] + }, + { + 'hash': {'ipv4': 'ipv4'}, + 'ans_attr': 'ipv4', + 'cfg_attr': 'ipv4-attrs', + 'delete_op': 'PATCH', + 'map_subattrs': [ + { + 'ans_attr': 'ipv4_dst_ip', + 'cfg_attr': 'ipv4-dst-ip', + 'dft_value': False + }, + { + 'ans_attr': 'ipv4_src_ip', + 'cfg_attr': 'ipv4-src-ip', + 'dft_value': False + }, + { + 'ans_attr': 'ipv4_ip_proto', + 'cfg_attr': 'ipv4-ip-proto', + 'dft_value': False + }, + { + 'ans_attr': 'ipv4_l4_dst_port', + 'cfg_attr': 'ipv4-l4-dst-port', + 'dft_value': False + }, + { + 'ans_attr': 'ipv4_l4_src_port', + 'cfg_attr': 'ipv4-l4-src-port', + 'dft_value': False + }, + { + 'ans_attr': 'ipv4_symmetric', + 'cfg_attr': 'ipv4-symmetric', + 'dft_value': False + } + ] + }, + { + 'hash': {'ipv6': 'ipv6'}, + 'ans_attr': 'ipv6', + 'cfg_attr': 'ipv6-attrs', + 'delete_op': 'PATCH', + 'map_subattrs': [ + { + 'ans_attr': 'ipv6_dst_ip', + 'cfg_attr': 'ipv6-dst-ip', + 'dft_value': False + }, + { + 'ans_attr': 'ipv6_src_ip', + 'cfg_attr': 'ipv6-src-ip', + 'dft_value': False + }, + { + 'ans_attr': 'ipv6_next_hdr', + 'cfg_attr': 'ipv6-next-hdr', + 'dft_value': False + }, + { + 'ans_attr': 'ipv6_l4_dst_port', + 'cfg_attr': 'ipv6-l4-dst-port', + 'dft_value': False + }, + { + 'ans_attr': 'ipv6_l4_src_port', + 'cfg_attr': 'ipv6-l4-src-port', + 'dft_value': False + }, + { + 'ans_attr': 'ipv6_symmetric', + 'cfg_attr': 'ipv6-symmetric', + 'dft_value': False + } + ] + } +] + + +class Loadshare_modeFacts(object): + """ The sonic loadshare_mode fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Loadshare_modeArgs.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_loadshare_mode_config(self): + """Get all IP ECMP load share mode configuration in chassis""" + request = [{"path": "data/openconfig-loadshare-mode-ext:loadshare", "method": GET}] + try: + response = edit_config(self._module, to_request(self._module, request)) + except ConnectionError as exc: + self._module.fail_json(msg=str(exc), code=exc.code) + if ('openconfig-loadshare-mode-ext:loadshare' in response[0][1]): + data = response[0][1]['openconfig-loadshare-mode-ext:loadshare'] + 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_loadshare_mode_config() + + if data: + loadshare_mode_facts = self.get_loadshare_mode_facts(data) + else: + loadshare_mode_facts = {} + + obj = self.render_config(self.generated_spec, loadshare_mode_facts) + + ansible_facts['ansible_network_resources'].pop('loadshare_mode', None) + facts = {} + if obj: + params = utils.validate_config(self.argument_spec, {'config': obj}) + facts['loadshare_mode'] = 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_loadshare_mode_facts(self, config): + + lsm_facts = dict() + + for amap in LOADSHARE_MODE_ATTR_MAP: + ans_attr = amap['ans_attr'] + cfg_attr = amap['cfg_attr'] + cfg_subattr = amap['cfg_subattr'] + lsm_facts[ans_attr] = (config.get(cfg_attr, {}) + .get('config', {}) + .get(cfg_subattr, None)) + + for amap in LOADSHARE_MODE_DICT_MAP: + ans_attr = amap['ans_attr'] + cfg_attr = amap['cfg_attr'] + + amap_subattrs = amap['map_subattrs'] + lsm_subfacts = dict() + for sub_amap in amap_subattrs: + ans_subattr = sub_amap['ans_attr'] + cfg_subattr = sub_amap['cfg_attr'] + lsm_subfacts[ans_subattr] = (config.get(cfg_attr, {}) + .get('config', {}) + .get(cfg_subattr, None)) + lsm_facts[ans_attr] = lsm_subfacts + + loadshare_mode_facts = remove_empties(lsm_facts) + return loadshare_mode_facts diff --git a/plugins/modules/sonic_facts.py b/plugins/modules/sonic_facts.py index 02bb16336..159cae133 100644 --- a/plugins/modules/sonic_facts.py +++ b/plugins/modules/sonic_facts.py @@ -109,6 +109,7 @@ - qos_interfaces - pim_global - pim_interfaces + - loadshare_mode - login_lockout """ diff --git a/plugins/modules/sonic_loadshare_mode.py b/plugins/modules/sonic_loadshare_mode.py new file mode 100644 index 000000000..b12cbeb40 --- /dev/null +++ b/plugins/modules/sonic_loadshare_mode.py @@ -0,0 +1,376 @@ +#!/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_loadshare_mode +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = """ +--- +module: sonic_loadshare_mode +version_added: 3.0.0 +author: "M. Zhang (@mingjunzhang2019)" +notes: +- Tested against Enterprise SONiC Distribution by Dell Technologies. +- Supports C(check_mode). +short_description: IP ECMP load share mode configuration handling for SONiC. +description: + - This module provides configuration management for IP ECMP load share mode on + - devices running SONiC. +options: + config: + description: + - Specifies IP ECMP load share mode configuration. + type: dict + suboptions: + hash_algorithm: + description: + - Load share hash algorithm. + type: str + choices: + - CRC + - XOR + - CRC_32LO + - CRC_32HI + - CRC_CCITT + - CRC_XOR + - JENKINS_HASH_LO + - JENKINS_HASH_HI + hash_ingress_port: + description: + - Load share hash ingress port. + type: bool + hash_offset: + description: + - Load share hash offset. + type: dict + suboptions: + offset: + description: + - IP ECMP hash offset value. + - The range of values is from 0 to 15. + type: int + flow_based: + description: + - Enable flow-based IP ECMP hashing. + type: bool + hash_roce_qpn: + description: + - Load share ROCE Queue-Pair Number. + type: bool + hash_seed: + description: + - IP ECMP hash seed value. + - The range of values is from 0 to 16777215. + type: int + ipv4: + description: + - IPv4 ECMP Load share hash parameters. + type: dict + suboptions: + ipv4_dst_ip: + description: + - IPv4 destination IP address. + type: bool + ipv4_src_ip: + description: + - IPv4 source IP address. + type: bool + ipv4_ip_proto: + description: + - IPv4 protocol. + type: bool + ipv4_l4_dst_port: + description: + - IPv4 L4 destination port. + type: bool + ipv4_l4_src_port: + description: + - IPv4 L4 source port. + type: bool + ipv4_symmetric: + description: + - IPv4 symmetric hash mode. + type: bool + ipv6: + description: + - IPv6 ECMP Load share hash parameters. + type: dict + suboptions: + ipv6_dst_ip: + description: + - IPv6 destination IP address. + type: bool + ipv6_src_ip: + description: + - IPv6 source IP address. + type: bool + ipv6_next_hdr: + description: + - IPv6 protocol. + type: bool + ipv6_l4_dst_port: + description: + - IPv6 L4 destination port. + type: bool + ipv6_l4_src_port: + description: + - IPv6 L4 source port. + type: bool + ipv6_symmetric: + description: + - IPv6 symmetric hash mode. + type: bool + state: + description: + - Specifies the operation to be performed on the load share mode configured on the device. + - In case of merged, the input configuration will be merged with the existing load share mode on the device. + - In case of deleted, the existing load share mode configuration will be removed from the device. + - In case of overridden, all existing load share mode will be deleted and + - the specified input configuration will be add. + - In case of replaced, the existing load share mode on the device will be replaced + - by the specified input configuration. + type: str + choices: + - merged + - deleted + - replaced + - overridden + default: merged +""" +EXAMPLES = """ +# +# Using deleted +# +# Before state: +# ------------- +# +#sonic# show running-configuration | grep load-share +#ip load-share hash seed 8888 +#ip load-share hash ipv4 ipv4-dst-ip +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash ipv4 ipv4-ip-proto +#ip load-share hash ipv4 ipv4-l4-src-port +#ip load-share hash ipv4 ipv4-l4-dst-port +#ip load-share hash algorithm CRC +#ip load-share hash ingress-port +# +- name: Merge some configuration + sonic_loadshare_mode: + config: + hash_algorithm: CRC + hash_offset: + offset: 12 + flow_based: True + hash_roce_qpn: True + hash_seed: 8888 + ipv4: + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + state: deleted +# +# After state: +# ------------ +# +#sonic# show running-configuration | grep load-share +#ip load-share hash ipv4 ipv4-dst-ip +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash ipv4 ipv4-ip-proto +#ip load-share hash ingress-port +# +# Using merged +# +# Before state: +# ------------- +# +#sonic# show running-configuration | grep load-share +#ip load-share hash seed 8888 +#ip load-share hash ipv4 ipv4-dst-ip +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash algorithm CRC +# +- name: Merge some configuration + sonic_loadshare_mode: + config: + hash_algorithm: CRC_32LO + hash_ingress_port: True + hash_offset: + offset: 12 + flow_based: True + hash_roce_qpn: True + hash_seed: 9999 + ipv4: + ipv4_src_ip: True + ipv4_ip_proto: True + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + state: merged +# +# After state: +# ------------ +# +#sonic# show running-configuration | grep load-share +#ip load-share hash seed 9999 +#ip load-share hash ipv4 ipv4-dst-ip +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash ipv4 ipv4-ip-proto +#ip load-share hash ipv4 ipv4-l4-src-port +#ip load-share hash ipv4 ipv4-l4-dst-port +#ip load-share hash algorithm CRC_32LO +#ip load-share hash ingress-port +# +# Using replaced +# +# Before state: +# ------------- +# +#sonic# show running-configuration | grep load-share +#ip load-share hash offset flow-based +#ip load-share hash seed 8888 +#ip load-share hash ipv4 ipv4-dst-ip +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash algorithm CRC +#ip load-share hash ingress-port +# +- name: Merge some configuration + sonic_loadshare_mode: + config: + hash_algorithm: CRC + hash_ingress_port: True + hash_offset: + flow_based: True + hash_seed: 8888 + ipv4: + ipv4_src_ip: True + ipv4_ip_proto: True + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + state: replaced +# +# After state: +# ------------ +# +#sonic# show running-configuration | grep load-share +#ip load-share hash offset flow-based +#ip load-share hash seed 8888 +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash ipv4 ipv4-ip-proto +#ip load-share hash ipv4 ipv4-l4-src-port +#ip load-share hash ipv4 ipv4-l4-dst-port +#ip load-share hash algorithm CRC +#ip load-share hash ingress-port +# +# Using overridden +# +# Before state: +# ------------- +# +#sonic# show running-configuration | grep load-share +#ip load-share hash seed 8888 +#ip load-share hash ipv4 ipv4-dst-ip +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash algorithm CRC +# +- name: Merge some configuration + sonic_loadshare_mode: + config: + hash_algorithm: CRC_32LO + hash_ingress_port: True + hash_offset: + flow_based: True + hash_seed: 9999 + ipv4: + ipv4_src_ip: True + ipv4_ip_proto: True + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + state: overridden +# +# After state: +# ------------ +# +#sonic# show running-configuration | grep load-share +#ip load-share hash offset flow-based +#ip load-share hash seed 9999 +#ip load-share hash ipv4 ipv4-src-ip +#ip load-share hash ipv4 ipv4-ip-proto +#ip load-share hash ipv4 ipv4-l4-src-port +#ip load-share hash ipv4 ipv4-l4-dst-port +#ip load-share hash algorithm CRC_32LO +#ip load-share hash ingress-port +# +""" +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.loadshare_mode.loadshare_mode import Loadshare_modeArgs +from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.loadshare_mode.loadshare_mode import Loadshare_mode + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Loadshare_modeArgs.argument_spec, + supports_check_mode=True) + + result = Loadshare_mode(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/regression/roles/sonic_loadshare_mode/defaults/main.yml b/tests/regression/roles/sonic_loadshare_mode/defaults/main.yml new file mode 100644 index 000000000..8ff41b3d1 --- /dev/null +++ b/tests/regression/roles/sonic_loadshare_mode/defaults/main.yml @@ -0,0 +1,90 @@ +--- +ansible_connection: httpapi +module_name: loadshare_mode + +tests: + - name: test_case_01 + description: Configure some loadshare modes + state: merged + input: + hash_ingress_port: True + hash_algorithm: CRC + ipv4: + ipv4_src_ip: True + ipv4_dst_ip: True + ipv6: + ipv6_src_ip: True + ipv6_dst_ip: True + + - name: test_case_02 + description: Configure more loadshare modes + state: merged + input: + hash_algorithm: CRC_32LO + hash_offset: + offset: 12 + flow_based: True + hash_roce_qpn: True + hash_seed: 8888 + ipv4: + ipv4_ip_proto: True + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + ipv4_symmetric: False + + - name: test_case_03 + description: Replace some loadshare mode configuration + state: replaced + input: + hash_algorithm: CRC_32LO + hash_ingress_port: True + hash_offset: + offset: 12 + flow_based: True + hash_roce_qpn: True + hash_seed: 8888 + ipv4: + ipv4_dst_ip: True + ipv4_src_ip: True + + - name: test_case_04 + description: Override loadshare mode configuration + state: overridden + input: + hash_algorithm: CRC_32HI + hash_ingress_port: True + hash_offset: + offset: 12 + flow_based: True + hash_roce_qpn: True + hash_seed: 8888 + ipv4: + ipv4_dst_ip: True + ipv4_src_ip: True + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + ipv6: + ipv6_dst_ip: True + ipv6_src_ip: True + ipv6_l4_dst_port: True + ipv6_l4_src_port: True + + - name: test_case_05 + description: Delete some loadshare mode configuration + state: deleted + input: + hash_algorithm: CRC_32HI + hash_ingress_port: True + hash_roce_qpn: True + hash_seed: 8888 + ipv4: + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + ipv6: + ipv6_l4_dst_port: True + ipv6_l4_src_port: True + + - name: test_case_6 + description: Delete all loadshare mode configuration + state: deleted + input: {} diff --git a/tests/regression/roles/sonic_loadshare_mode/meta/main.yaml b/tests/regression/roles/sonic_loadshare_mode/meta/main.yaml new file mode 100644 index 000000000..611fd54d2 --- /dev/null +++ b/tests/regression/roles/sonic_loadshare_mode/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_loadshare_mode/tasks/main.yml b/tests/regression/roles/sonic_loadshare_mode/tasks/main.yml new file mode 100644 index 000000000..e4663e391 --- /dev/null +++ b/tests/regression/roles/sonic_loadshare_mode/tasks/main.yml @@ -0,0 +1,8 @@ +- debug: msg="sonic_loadshare_mode Test started ..." + +- name: Preparations tests + include_tasks: preparation_tests.yaml + +- name: "Test {{ module_name }} started ..." + include_tasks: tasks_template.yaml + loop: "{{ tests }}" diff --git a/tests/regression/roles/sonic_loadshare_mode/tasks/preparation_tests.yaml b/tests/regression/roles/sonic_loadshare_mode/tasks/preparation_tests.yaml new file mode 100644 index 000000000..44f4cde70 --- /dev/null +++ b/tests/regression/roles/sonic_loadshare_mode/tasks/preparation_tests.yaml @@ -0,0 +1,5 @@ +- name: Delete existing IP loadshare mode configurations + sonic_loadshare_mode: + config: {} + state: deleted + ignore_errors: yes diff --git a/tests/regression/roles/sonic_loadshare_mode/tasks/tasks_template.yaml b/tests/regression/roles/sonic_loadshare_mode/tasks/tasks_template.yaml new file mode 100644 index 000000000..edd370d38 --- /dev/null +++ b/tests/regression/roles/sonic_loadshare_mode/tasks/tasks_template.yaml @@ -0,0 +1,21 @@ +- name: "{{ item.name}} , {{ item.description}}" + sonic_loadshare_mode: + 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_loadshare_mode: + 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 065f0e0f8..33c985e2a 100644 --- a/tests/regression/test.yaml +++ b/tests/regression/test.yaml @@ -68,4 +68,5 @@ - sonic_pim_interfaces - sonic_login_lockout - sonic_poe + - sonic_loadshare_mode - test_reports diff --git a/tests/unit/modules/network/sonic/fixtures/sonic_loadshare_mode.yaml b/tests/unit/modules/network/sonic/fixtures/sonic_loadshare_mode.yaml new file mode 100644 index 000000000..7983006bd --- /dev/null +++ b/tests/unit/modules/network/sonic/fixtures/sonic_loadshare_mode.yaml @@ -0,0 +1,323 @@ +--- +merged_01: + module_args: + config: + hash_algorithm: CRC_32HI + hash_ingress_port: True + hash_offset: + offset: 12 + flow_based: True + hash_roce_qpn: True + hash_seed: 8888 + ipv4: + ipv4_src_ip: True + ipv4_ip_proto: True + ipv4_l4_dst_port: True + ipv4_l4_src_port: True + ipv4_symmetric: False + existing_loadshare_mode_config: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + response: + code: 200 + value: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC_32LO + ingress-port: + config: + ingress-port: True + ipv4-attrs: + config: + ipv4-src-ip: True + ipv4-dst-ip: True + ipv4-l4-src-port: True + ipv4-l4-dst-port: True + ipv4-ip-proto: True + ipv4-symmetric: True + expected_config_requests: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "patch" + data: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC_32HI + roce-attrs: + config: + qpn: True + hash: hash + seed-attrs: + config: + ecmp-hash-seed: 8888 + hash: hash + offset-attrs: + config: + ecmp-hash-offset: 12 + ecmp-hash-flow-based: True + hash: hash + ipv4-attrs: + config: + ipv4-symmetric: False + ipv4: ipv4 +deleted_01: + module_args: + state: deleted + config: + existing_loadshare_mode_config: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + response: + code: 200 + value: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC + ipv4-attrs: + config: + ipv4-src-ip: True + ipv4-dst-ip: True + ipv4-l4-src-port: True + ipv4-l4-dst-port: True + expected_config_requests: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "delete" + data: + +deleted_02: + module_args: + state: deleted + config: + hash_algorithm: CRC + hash_ingress_port: True + hash_roce_qpn: True + ipv4: + ipv4_src_ip: True + existing_loadshare_mode_config: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + response: + code: 200 + value: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC + ingress-port: + config: + ingress-port: True + roce-attrs: + config: + qpn: True + ipv4-attrs: + config: + ipv4-src-ip: True + ipv4-dst-ip: True + ipv4-l4-src-port: True + ipv4-l4-dst-port: True + expected_config_requests: + - path: "data/openconfig-loadshare-mode-ext:loadshare/hash-algorithm" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare/roce-attrs" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare/ingress-port" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "patch" + data: + openconfig-loadshare-mode-ext:loadshare: + ipv4-attrs: + config: + ipv4-src-ip: False + ipv4: ipv4 + +replaced_01: + module_args: + state: replaced + config: + hash_algorithm: CRC_32LO + hash_ingress_port: True + hash_offset: + offset: 12 + flow_based: True + hash_roce_qpn: True + hash_seed: 8888 + ipv4: + ipv4_src_ip: True + ipv4_dst_ip: True + ipv4_symmetric: False + existing_loadshare_mode_config: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + response: + code: 200 + value: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC_32HI + ingress-port: + config: + ingress-port: True + roce-attrs: + config: + qpn: True + seed-attrs: + config: + ecmp-hash-seed: 8888 + ipv4-attrs: + config: + ipv4-src-ip: True + ipv4-dst-ip: True + ipv4-l4-src-port: True + ipv4-l4-dst-port: True + ipv4-symmetric: False + expected_config_requests: + - path: "data/openconfig-loadshare-mode-ext:loadshare/hash-algorithm" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare/ingress-port" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare/roce-attrs" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare/seed-attrs" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "patch" + data: + openconfig-loadshare-mode-ext:loadshare: + ipv4-attrs: + config: + ipv4-dst-ip: False + ipv4-src-ip: False + ipv4-l4-dst-port: False + ipv4-l4-src-port: False + ipv4-symmetric: False + ipv4: ipv4 + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "patch" + data: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC_32LO + ingress-port: + config: + ingress-port: True + hash: hash + roce-attrs: + config: + qpn: True + hash: hash + seed-attrs: + config: + ecmp-hash-seed: 8888 + hash: hash + offset-attrs: + config: + ecmp-hash-offset: 12 + ecmp-hash-flow-based: True + hash: hash + ipv4-attrs: + config: + ipv4-dst-ip: True + ipv4-src-ip: True + ipv4-symmetric: False + ipv4: ipv4 + +overridden_01: + module_args: + state: overridden + config: + hash_algorithm: CRC + hash_offset: + offset: 12 + hash_roce_qpn: True + hash_seed: 9999 + ipv4: + ipv4_src_ip: True + ipv4_dst_ip: True + ipv4_ip_proto: True + existing_loadshare_mode_config: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + response: + code: 200 + value: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + response: + code: 200 + value: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC_32LO + ingress-port: + config: + ingress-port: True + roce-attrs: + config: + qpn: True + seed-attrs: + config: + ecmp-hash-seed: 8888 + offset-attrs: + config: + ecmp-hash-offset: 12 + ecmp-hash-flow-based: True + ipv4-attrs: + config: + ipv4-src-ip: True + ipv4-dst-ip: True + ipv4-l4-src-port: False + ipv4-l4-dst-port: False + ipv4-ip-proto: False + ipv4-symmetric: False + expected_config_requests: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "delete" + data: + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "patch" + data: + openconfig-loadshare-mode-ext:loadshare: + ipv4-attrs: + config: + ipv4-dst-ip: False + ipv4-src-ip: False + ipv4-ip-proto: False + ipv4-l4-dst-port: False + ipv4-l4-src-port: False + ipv4-symmetric: False + ipv4: ipv4 + - path: "data/openconfig-loadshare-mode-ext:loadshare" + method: "patch" + data: + openconfig-loadshare-mode-ext:loadshare: + hash-algorithm: + config: + algorithm: CRC + roce-attrs: + config: + qpn: True + hash: hash + seed-attrs: + config: + ecmp-hash-seed: 9999 + hash: hash + offset-attrs: + config: + ecmp-hash-offset: 12 + hash: hash + ipv4-attrs: + config: + ipv4-dst-ip: True + ipv4-src-ip: True + ipv4-ip-proto: True + ipv4-l4-dst-port: False + ipv4-l4-src-port: False + ipv4-symmetric: False + ipv4: ipv4 diff --git a/tests/unit/modules/network/sonic/test_sonic_loadshare_mode.py b/tests/unit/modules/network/sonic/test_sonic_loadshare_mode.py new file mode 100644 index 000000000..c89d0de11 --- /dev/null +++ b/tests/unit/modules/network/sonic/test_sonic_loadshare_mode.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_loadshare_mode, +) +from ansible_collections.dellemc.enterprise_sonic.tests.unit.modules.utils import ( + set_module_args, +) +from .sonic_module import TestSonicModule + + +class TestSonicLoadshare_modeModule(TestSonicModule): + module = sonic_loadshare_mode + + @classmethod + def setUpClass(cls): + cls.mock_facts_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.loadshare_mode.loadshare_mode.edit_config" + ) + cls.mock_config_edit_config = patch( + "ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.config.loadshare_mode.loadshare_mode.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_loadshare_mode.yaml') + + def setUp(self): + super(TestSonicLoadshare_modeModule, 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(TestSonicLoadshare_modeModule, self).tearDown() + self.mock_facts_edit_config.stop() + self.mock_config_edit_config.stop() + self.mock_get_interface_naming_mode.stop() + + def test_sonic_loadshare_mode_merged_01(self): + set_module_args(self.fixture_data['merged_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['merged_01']['existing_loadshare_mode_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_loadshare_mode_deleted_01(self): + set_module_args(self.fixture_data['deleted_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_01']['existing_loadshare_mode_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_loadshare_mode_deleted_02(self): + set_module_args(self.fixture_data['deleted_02']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['deleted_02']['existing_loadshare_mode_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_loadshare_mode_replaced_01(self): + set_module_args(self.fixture_data['replaced_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['replaced_01']['existing_loadshare_mode_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_loadshare_mode_overridden_01(self): + set_module_args(self.fixture_data['overridden_01']['module_args']) + self.initialize_facts_get_requests(self.fixture_data['overridden_01']['existing_loadshare_mode_config']) + self.initialize_config_requests(self.fixture_data['overridden_01']['expected_config_requests']) + result = self.execute_module(changed=True) + self.validate_config_requests() From effd9c866d6477e0427b5693fa623e31c432e16e Mon Sep 17 00:00:00 2001 From: mingjunzhang2019 Date: Mon, 26 Aug 2024 12:44:41 -0500 Subject: [PATCH 2/3] Resolve merge conflicts --- plugins/module_utils/network/sonic/facts/facts.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/module_utils/network/sonic/facts/facts.py b/plugins/module_utils/network/sonic/facts/facts.py index 7e356a979..0c6025fc6 100644 --- a/plugins/module_utils/network/sonic/facts/facts.py +++ b/plugins/module_utils/network/sonic/facts/facts.py @@ -76,12 +76,9 @@ 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 -<<<<<<< HEAD from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.loadshare_mode.loadshare_mode import Loadshare_modeFacts -======= from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.mgmt_servers.mgmt_servers import Mgmt_serversFacts from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.ospf_area.ospf_area import Ospf_areaFacts ->>>>>>> main FACT_LEGACY_SUBSETS = {} FACT_RESOURCE_SUBSETS = dict( From b4b388d2d3078283f2aa0f7555e3870f523ed8df Mon Sep 17 00:00:00 2001 From: mingjunzhang2019 Date: Tue, 27 Aug 2024 12:23:46 -0500 Subject: [PATCH 3/3] Add fragment file --- .../fragments/442-sonic-ip-ecmp-loadshare-mode-module.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/442-sonic-ip-ecmp-loadshare-mode-module.yaml diff --git a/changelogs/fragments/442-sonic-ip-ecmp-loadshare-mode-module.yaml b/changelogs/fragments/442-sonic-ip-ecmp-loadshare-mode-module.yaml new file mode 100644 index 000000000..5ddc379f1 --- /dev/null +++ b/changelogs/fragments/442-sonic-ip-ecmp-loadshare-mode-module.yaml @@ -0,0 +1,2 @@ +major_changes: + - sonic_loadshare_mode - Add 'sonic_loadshare_mode' module to Dell enterprise SONiC collection (https://github.com/ansible-collections/dellemc.enterprise_sonic/pull/442).