diff --git a/lib/ansible/modules/network/onyx/onyx_traffic_class.py b/lib/ansible/modules/network/onyx/onyx_traffic_class.py new file mode 100644 index 00000000000..5cc70b8d15d --- /dev/null +++ b/lib/ansible/modules/network/onyx/onyx_traffic_class.py @@ -0,0 +1,326 @@ +#!/usr/bin/python +# +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: onyx_traffic_class +version_added: "2.9" +author: "Anas Badaha (@anasb)" +short_description: Configures Traffic Class +description: + - This module provides declarative management of Traffic Class configuration + on Mellanox ONYX network devices. +options: + state: + description: + - enable congestion control on interface. + choices: ['enabled', 'disabled'] + default: enabled + interfaces: + description: + - list of interfaces name. + required: true + tc: + description: + - traffic class, range 0-7. + required: true + congestion_control: + description: + - configure congestion control on interface. + suboptions: + control: + description: + - congestion control type. + choices: ['red', 'ecn', 'both'] + required: true + threshold_mode: + description: + - congestion control threshold mode. + choices: ['absolute', 'relative'] + required: true + min_threshold: + description: + - Set minimum-threshold value (in KBs) for marking traffic-class queue. + required: true + max_threshold: + description: + - Set maximum-threshold value (in KBs) for marking traffic-class queue. + required: true + dcb: + description: + - configure dcb control on interface. + suboptions: + mode: + description: + - dcb control mode. + choices: ['strict', 'wrr'] + required: true + weight: + description: + - Relevant only for wrr mode. +""" + +EXAMPLES = """ +- name: configure traffic class + onyx_traffic_class: + interfaces: + - Eth1/1 + - Eth1/2 + tc: 3 + congestion_control: + control: ecn + threshold_mode: absolute + min_threshold: 500 + max_threshold: 1500 + dcb: + mode: strict +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device. + returned: always + type: list + sample: + - interface ethernet 1/15 traffic-class 3 congestion-control ecn minimum-absolute 150 maximum-absolute 1500 + - interface ethernet 1/16 traffic-class 3 congestion-control ecn minimum-absolute 150 maximum-absolute 1500 + - interface mlag-port-channel 7 traffic-class 3 congestion-control ecn minimum-absolute 150 maximum-absolute 1500 + - interface port-channel 1 traffic-class 3 congestion-control ecn minimum-absolute 150 maximum-absolute 1500 + - interface ethernet 1/15 traffic-class 3 dcb ets strict + - interface ethernet 1/16 traffic-class 3 dcb ets strict + - interface mlag-port-channel 7 traffic-class 3 dcb ets strict + - interface port-channel 1 traffic-class 3 dcb ets strict +""" + +import re +from ansible.module_utils.six import iteritems +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.onyx.onyx import show_cmd +from ansible.module_utils.network.onyx.onyx import BaseOnyxModule + + +class OnyxTrafficClassModule(BaseOnyxModule): + + IF_ETH_REGEX = re.compile(r"^Eth(\d+\/\d+|Eth\d+\/\d+\d+)$") + IF_PO_REGEX = re.compile(r"^Po(\d+)$") + MLAG_NAME_REGEX = re.compile(r"^Mpo(\d+)$") + + IF_TYPE_ETH = "ethernet" + PORT_CHANNEL = "port-channel" + MLAG_PORT_CHANNEL = "mlag-port-channel" + + IF_TYPE_MAP = { + IF_TYPE_ETH: IF_ETH_REGEX, + PORT_CHANNEL: IF_PO_REGEX, + MLAG_PORT_CHANNEL: MLAG_NAME_REGEX + } + + def init_module(self): + """ initialize module + """ + congestion_control_spec = dict(control=dict(choices=['red', 'ecn', 'both'], required=True), + threshold_mode=dict(choices=['absolute', 'relative'], required=True), + min_threshold=dict(type=int, required=True), + max_threshold=dict(type=int, required=True)) + + dcb_spec = dict(mode=dict(choices=['strict', 'wrr'], required=True), + weight=dict(type=int)) + + element_spec = dict( + interfaces=dict(type='list', required=True), + tc=dict(type=int, required=True), + congestion_control=dict(type='dict', options=congestion_control_spec), + dcb=dict(type='dict', options=dcb_spec), + state=dict(choices=['enabled', 'disabled'], default='enabled')) + + argument_spec = dict() + argument_spec.update(element_spec) + self._module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True) + + def get_required_config(self): + module_params = self._module.params + self._required_config = dict(module_params) + self.validate_param_values(self._required_config) + + def validate_tc(self, value): + if value and not 0 <= int(value) <= 7: + self._module.fail_json(msg='tc value must be between 0 and 7') + + def validate_param_values(self, obj, param=None): + dcb = obj.get("dcb") + if dcb is not None: + dcb_mode = dcb.get("mode") + weight = dcb.get("weight") + if dcb_mode == "wrr" and weight is None: + self._module.fail_json(msg='User should send weight attribute when dcb mode is wrr') + super(OnyxTrafficClassModule, self).validate_param_values(obj, param) + + def _get_interface_type(self, if_name): + if_type = None + if_id = None + for interface_type, interface_regex in iteritems(self.IF_TYPE_MAP): + match = interface_regex.match(if_name) + if match: + if_type = interface_type + if_id = match.group(1) + break + return if_type, if_id + + def _set_interface_congestion_control_config(self, interface_congestion_control_config, + interface, if_type, if_id): + tc = self._required_config.get("tc") + interface_dcb_ets = self._show_interface_dcb_ets(if_type, if_id)[0].get(interface) + if interface_dcb_ets is None: + dcb = dict() + else: + ets_per_tc = interface_dcb_ets[2].get("ETS per TC") + tc_config = ets_per_tc[0].get(str(tc)) + dcb_mode = tc_config[0].get("S.Mode") + dcb_weight = int(tc_config[0].get("W")) + dcb = dict(mode=dcb_mode.lower(), weight=dcb_weight) + + interface_congestion_control_config = interface_congestion_control_config[tc + 1] + mode = interface_congestion_control_config.get("Mode") + if mode == "none": + self._current_config[interface] = dict(state="disabled", dcb=dcb, if_type=if_type, if_id=if_id) + return + + threshold_mode = interface_congestion_control_config.get("Threshold mode") + max_threshold = interface_congestion_control_config.get("Maximum threshold") + min_threshold = interface_congestion_control_config.get("Minimum threshold") + + if threshold_mode == "absolute": + delimiter = ' ' + else: + delimiter = '%' + min_value = int(min_threshold.split(delimiter)[0]) + max_malue = int(max_threshold.split(delimiter)[0]) + congestion_control = dict(control=mode.lower(), threshold_mode=threshold_mode, + min_threshold=min_value, max_threshold=max_malue) + + self._current_config[interface] = dict(state="enabled", congestion_control=congestion_control, + dcb=dcb, if_type=if_type, if_id=if_id) + + def _show_interface_congestion_control(self, if_type, interface): + cmd = "show interfaces {0} {1} congestion-control".format(if_type, interface) + return show_cmd(self._module, cmd, json_fmt=True, fail_on_error=False) + + def _show_interface_dcb_ets(self, if_type, interface): + cmd = "show dcb ets interface {0} {1}".format(if_type, interface) + return show_cmd(self._module, cmd, json_fmt=True, fail_on_error=False) + + def load_current_config(self): + self._current_config = dict() + for interface in self._required_config.get("interfaces"): + if_type, if_id = self._get_interface_type(interface) + if not if_id: + self._module.fail_json( + msg='unsupported interface: {0}'.format(interface)) + interface_congestion_control_config = self._show_interface_congestion_control(if_type, if_id) + if interface_congestion_control_config is not None: + self._set_interface_congestion_control_config(interface_congestion_control_config, + interface, if_type, if_id) + else: + self._module.fail_json( + msg='Interface {0} does not exist on switch'.format(interface)) + + def generate_commands(self): + state = self._required_config.get("state") + tc = self._required_config.get("tc") + interfaces = self._required_config.get("interfaces") + for interface in interfaces: + current_interface = self._current_config.get(interface) + current_state = current_interface.get("state") + if_type = current_interface.get("if_type") + if_id = current_interface.get("if_id") + if state == "disabled": + if current_state == "enabled": + self._commands.append('interface {0} {1} no traffic-class {2} congestion-control'.format(if_type, if_id, tc)) + continue + + congestion_control = self._required_config.get("congestion_control") + + if congestion_control is not None: + control = congestion_control.get("control") + current_congestion_control = current_interface.get("congestion_control") + threshold_mode = congestion_control.get("threshold_mode") + min_threshold = congestion_control.get("min_threshold") + max_threshold = congestion_control.get("max_threshold") + if current_congestion_control is None: + self._threshold_mode_generate_cmds_mappers(threshold_mode, if_type, if_id, tc, + control, min_threshold, max_threshold) + else: + current_control = current_congestion_control.get("control") + curr_threshold_mode = current_congestion_control.get("threshold_mode") + curr_min_threshold = current_congestion_control.get("min_threshold") + curr_max_threshold = current_congestion_control.get("max_threshold") + + if control != current_control: + self._threshold_mode_generate_cmds_mappers(threshold_mode, if_type, if_id, tc, + control, min_threshold, max_threshold) + else: + if threshold_mode != curr_threshold_mode: + self._threshold_mode_generate_cmds_mappers(threshold_mode, if_type, if_id, tc, + control, min_threshold, max_threshold) + elif min_threshold != curr_min_threshold or max_threshold != curr_max_threshold: + self._threshold_mode_generate_cmds_mappers(threshold_mode, if_type, if_id, tc, + control, min_threshold, max_threshold) + + dcb = self._required_config.get("dcb") + if dcb is not None: + dcb_mode = dcb.get("mode") + current_dcb = current_interface.get("dcb") + current_dcb_mode = current_dcb.get("mode") + if dcb_mode == "strict" and dcb_mode != current_dcb_mode: + self._commands.append('interface {0} {1} traffic-class {2} ' + 'dcb ets {3}'.format(if_type, if_id, tc, dcb_mode)) + elif dcb_mode == "wrr": + weight = dcb.get("weight") + current_weight = current_dcb.get("weight") + if dcb_mode != current_dcb_mode or weight != current_weight: + self._commands.append('interface {0} {1} traffic-class {2} ' + 'dcb ets {3} {4}'.format(if_type, if_id, tc, dcb_mode, weight)) + + def _threshold_mode_generate_cmds_mappers(self, threshold_mode, if_type, if_id, tc, + control, min_threshold, max_threshold): + if threshold_mode == 'absolute': + self._generate_congestion_control_absolute_cmds(if_type, if_id, tc, control, + min_threshold, max_threshold) + else: + self._generate_congestion_control_relative_cmds(if_type, if_id, tc, control, + min_threshold, max_threshold) + + def _generate_congestion_control_absolute_cmds(self, if_type, if_id, tc, control, + min_absolute, max_absolute): + self._commands.append('interface {0} {1} traffic-class {2} ' + 'congestion-control {3} minimum-absolute {4} ' + 'maximum-absolute {5}'.format(if_type, if_id, tc, control, + min_absolute, max_absolute)) + + def _generate_congestion_control_relative_cmds(self, if_type, if_id, tc, control, + min_relative, max_relative): + self._commands.append('interface {0} {1} traffic-class {2} ' + 'congestion-control {3} minimum-relative {4} ' + 'maximum-relative {5}'.format(if_type, if_id, tc, control, + min_relative, max_relative)) + + +def main(): + """ main entry point for module execution + """ + OnyxTrafficClassModule.main() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/onyx/fixtures/onyx_show_dcb_ets_interface.cfg b/test/units/modules/network/onyx/fixtures/onyx_show_dcb_ets_interface.cfg new file mode 100644 index 00000000000..b6bbc30f284 --- /dev/null +++ b/test/units/modules/network/onyx/fixtures/onyx_show_dcb_ets_interface.cfg @@ -0,0 +1,99 @@ +[ + { + "Eth1/1": [ + { + "Multicast unaware mapping": "disabled", + "Interface Bandwidth Shape [Mbps]": "N/A" + }, + { + "Flags": [ + { + "S.Mode": "Scheduling Mode [Strict/WRR]", + "Bw.Sh": "Bandwidth Shaper", + "D": "-", + "W": "Weight", + "Bw.Gr": "Bandwidth Guaranteed" + } + ] + }, + { + "ETS per TC": [ + { + "1": [ + { + "S.Mode": "WRR", + "BW Sh.(Mbps)": "N/A", + "W(%)": "17", + "BW Gr.(Mbps)": "0", + "W": "13" + } + ], + "0": [ + { + "S.Mode": "WRR", + "BW Sh.(Mbps)": "N/A", + "W(%)": "17", + "BW Gr.(Mbps)": "0", + "W": "12" + } + ], + "3": [ + { + "S.Mode": "Strict", + "BW Sh.(Mbps)": "N/A", + "W(%)": "0", + "BW Gr.(Mbps)": "0", + "W": "0" + } + ], + "2": [ + { + "S.Mode": "Strict", + "BW Sh.(Mbps)": "N/A", + "W(%)": "0", + "BW Gr.(Mbps)": "0", + "W": "0" + } + ], + "5": [ + { + "S.Mode": "WRR", + "BW Sh.(Mbps)": "N/A", + "W(%)": "17", + "BW Gr.(Mbps)": "0", + "W": "13" + } + ], + "4": [ + { + "S.Mode": "WRR", + "BW Sh.(Mbps)": "N/A", + "W(%)": "16", + "BW Gr.(Mbps)": "0", + "W": "12" + } + ], + "7": [ + { + "S.Mode": "WRR", + "BW Sh.(Mbps)": "N/A", + "W(%)": "17", + "BW Gr.(Mbps)": "0", + "W": "13" + } + ], + "6": [ + { + "S.Mode": "WRR", + "BW Sh.(Mbps)": "N/A", + "W(%)": "16", + "BW Gr.(Mbps)": "0", + "W": "12" + } + ] + } + ] + } + ] + } +] diff --git a/test/units/modules/network/onyx/fixtures/onyx_show_interface_congestion_control.cfg b/test/units/modules/network/onyx/fixtures/onyx_show_interface_congestion_control.cfg new file mode 100644 index 00000000000..e4c1207f313 --- /dev/null +++ b/test/units/modules/network/onyx/fixtures/onyx_show_interface_congestion_control.cfg @@ -0,0 +1,46 @@ +[ + { + "Interface ethernet": "1/1", + "ECN marked packets": "0" + }, + { + "header": "TC-0", + "Mode": "none" + }, + { + "header": "TC-1", + "Mode": "none" + }, + { + "Threshold mode": "relative", + "RED dropped packets": "0", + "header": "TC-2", + "Mode": "RED", + "Maximum threshold": "90%", + "Minimum threshold": "9%" + }, + { + "Threshold mode": "absolute", + "RED dropped packets": "0", + "header": "TC-3", + "Mode": "ECN", + "Maximum threshold": "1550 KB", + "Minimum threshold": "500 KB" + }, + { + "header": "TC-4", + "Mode": "none" + }, + { + "header": "TC-5", + "Mode": "none" + }, + { + "header": "TC-6", + "Mode": "none" + }, + { + "header": "TC-7", + "Mode": "none" + } +] diff --git a/test/units/modules/network/onyx/test_onyx_traffic_class.py b/test/units/modules/network/onyx/test_onyx_traffic_class.py new file mode 100644 index 00000000000..d62feca1377 --- /dev/null +++ b/test/units/modules/network/onyx/test_onyx_traffic_class.py @@ -0,0 +1,108 @@ +# +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import patch +from ansible.modules.network.onyx import onyx_traffic_class +from units.modules.utils import set_module_args +from .onyx_module import TestOnyxModule, load_fixture + + +class TestOnyxTrafficClassModule(TestOnyxModule): + + module = onyx_traffic_class + arp_suppression = True + + def setUp(self): + super(TestOnyxTrafficClassModule, self).setUp() + self.mock_get_congestion_control_config = patch.object( + onyx_traffic_class.OnyxTrafficClassModule, "_show_interface_congestion_control") + self.get_congestion_control_config = self.mock_get_congestion_control_config.start() + + self.mock_load_config = patch( + 'ansible.module_utils.network.onyx.onyx.load_config') + self.load_config = self.mock_load_config.start() + + self.mock_get_dcb_config = patch.object( + onyx_traffic_class.OnyxTrafficClassModule, "_show_interface_dcb_ets") + self.get_dcb_config = self.mock_get_dcb_config.start() + + def tearDown(self): + super(TestOnyxTrafficClassModule, self).tearDown() + self.mock_get_congestion_control_config.stop() + self.mock_load_config.stop() + self.mock_get_dcb_config.stop() + + def load_fixtures(self, commands=None, transport='cli'): + interfaces_congestion_control_config_file = 'onyx_show_interface_congestion_control.cfg' + interfaces_dcb_config_file = 'onyx_show_dcb_ets_interface.cfg' + interfaces_congestion_control_data = load_fixture(interfaces_congestion_control_config_file) + interfaces_dcb_config_data = load_fixture(interfaces_dcb_config_file) + self.get_congestion_control_config.return_value = interfaces_congestion_control_data + self.get_dcb_config.return_value = interfaces_dcb_config_data + self.load_config.return_value = None + + def test_configure_congestion_control_disabled_with_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=1, + congestion_control=dict(control="ecn", threshold_mode="absolute", + min_threshold=500, max_threshold=1500))) + commands = [ + "interface ethernet 1/1 traffic-class 1 congestion-control ecn minimum-absolute 500 maximum-absolute 1500" + ] + self.execute_module(changed=True, commands=commands) + + def test_configure_congestion_control_disabled_with_no_change(self): + set_module_args(dict(state="disabled", interfaces=["Eth1/1"], tc=0)) + + self.execute_module(changed=False) + + def test_configure_congestion_control_with_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=2, + congestion_control=dict(control="ecn", threshold_mode="relative", + min_threshold=9, max_threshold=88))) + commands = [ + "interface ethernet 1/1 traffic-class 2 congestion-control ecn minimum-relative 9 maximum-relative 88" + ] + self.execute_module(changed=True, commands=commands) + + def test_configure_congestion_control_absolute_with_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=3, + congestion_control=dict(control="ecn", threshold_mode="absolute", + min_threshold=500, max_threshold=1500))) + commands = [ + "interface ethernet 1/1 traffic-class 3 congestion-control ecn minimum-absolute 500 maximum-absolute 1500" + ] + self.execute_module(changed=True, commands=commands) + + def test_configure_congestion_control_with_no_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=3, + congestion_control=dict(control="ecn", threshold_mode="absolute", + min_threshold=500, max_threshold=1550))) + self.execute_module(changed=False) + + def test_configure_dcb_mode_with_no_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=3, dcb=dict(mode="strict"))) + self.execute_module(changed=False) + + def test_configure_dcb_strict_mode_with_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=1, dcb=dict(mode="strict"))) + commands = [ + "interface ethernet 1/1 traffic-class 1 dcb ets strict" + ] + self.execute_module(changed=True, commands=commands) + + def test_configure_dcb_wrr_mode_with_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=0, dcb=dict(mode="wrr", weight=10))) + commands = [ + "interface ethernet 1/1 traffic-class 0 dcb ets wrr 10" + ] + self.execute_module(changed=True, commands=commands) + + def test_configure_dcb_wrr_mode_with_no_change(self): + set_module_args(dict(interfaces=["Eth1/1"], tc=0, dcb=dict(mode="wrr", weight=12))) + + self.execute_module(changed=False)