diff --git a/lib/ansible/modules/network/mlnxos/mlnxos_pfc_interface.py b/lib/ansible/modules/network/mlnxos/mlnxos_pfc_interface.py new file mode 100644 index 00000000000..c117d5b3108 --- /dev/null +++ b/lib/ansible/modules/network/mlnxos/mlnxos_pfc_interface.py @@ -0,0 +1,205 @@ +#!/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: mlnxos_pfc_interface +version_added: "2.5" +author: "Samer Deeb (@samerd)" +short_description: Manage priority flow control on MLNX-OS network devices +description: + - This module provides declarative management of priority flow control (PFC) + on interfaces of Mellanox MLNX-OS network devices. +notes: + - Tested on MLNX-OS 3.6.4000 +options: + name: + description: + - Name of the interface PFC should be configured on. + aggregate: + description: List of interfaces PFC should be configured on. + purge: + description: + - Purge interfaces not defined in the aggregate parameter. + type: bool + default: false + state: + description: + - State of the PFC configuration. + default: enabled + choices: ['enabled', 'disabled'] +""" + +EXAMPLES = """ +- name: configure PFC + mlnxos_pfc_interface: + name: Eth1/1 + state: enabled +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device. + returned: always + type: list + sample: + - interface ethernet 1/17 dcb priority-flow-control mode on +""" +from copy import deepcopy +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.six import iteritems + +from ansible.module_utils.network.mlnxos.mlnxos import BaseMlnxosModule +from ansible.module_utils.network.mlnxos.mlnxos import show_cmd + + +class MlnxosPfcInterfaceModule(BaseMlnxosModule): + PFC_IF_REGEX = re.compile( + r"^(Eth\d+\/\d+)|(Eth\d+\/\d+\/\d+)|(Po\d+)|(Mpo\d+)$") + + _purge = False + + @classmethod + def _get_element_spec(cls): + return dict( + name=dict(type='str'), + state=dict(default='enabled', + choices=['enabled', 'disabled']), + ) + + @classmethod + def _get_aggregate_spec(cls, element_spec): + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + return aggregate_spec + + def init_module(self): + """ module initialization + """ + element_spec = self._get_element_spec() + aggregate_spec = self._get_aggregate_spec(element_spec) + argument_spec = dict( + aggregate=dict(type='list', elements='dict', + options=aggregate_spec), + purge=dict(default=False, type='bool'), + ) + argument_spec.update(element_spec) + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + self._module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + def get_required_config(self): + self._required_config = list() + module_params = self._module.params + aggregate = module_params.get('aggregate') + self._purge = module_params.get('purge', False) + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module_params[key] + self.validate_param_values(item, item) + req_item = item.copy() + self._required_config.append(req_item) + else: + params = { + 'name': module_params['name'], + 'state': module_params['state'], + } + self.validate_param_values(params) + self._required_config.append(params) + + def _create_if_pfc_data(self, if_name, if_pfc_data): + state = self.get_config_attr(if_pfc_data, "PFC oper") + state = state.lower() + return dict( + name=if_name, + state=state) + + def _get_pfc_config(self): + return show_cmd(self._module, "show dcb priority-flow-control") + + def load_current_config(self): + # called in base class in run function + self._current_config = dict() + pfc_config = self._get_pfc_config() + if not pfc_config: + return + if 'Table 2' in pfc_config: + pfc_config = pfc_config['Table 2'] + for if_name, if_pfc_data in iteritems(pfc_config): + match = self.PFC_IF_REGEX.match(if_name) + if not match: + continue + if if_pfc_data: + if_pfc_data = if_pfc_data[0] + self._current_config[if_name] = \ + self._create_if_pfc_data(if_name, if_pfc_data) + + def _get_interface_cmd_name(self, if_name): + if if_name.startswith('Eth'): + return if_name.replace("Eth", "ethernet ") + if if_name.startswith('Po'): + return if_name.replace("Po", "port-channel ") + if if_name.startswith('Mpo'): + return if_name.replace("Mpo", "mlag-port-channel ") + self._module.fail_json( + msg='invalid interface name: %s' % if_name) + + def _add_if_pfc_commands(self, if_name, req_state): + cmd_prefix = "interface %s " % self._get_interface_cmd_name(if_name) + + if req_state == 'disabled': + pfc_cmd = 'no dcb priority-flow-control mode force' + else: + pfc_cmd = 'dcb priority-flow-control mode on force' + self._commands.append(cmd_prefix + pfc_cmd) + + def _gen_pfc_commands(self, if_name, curr_conf, req_state): + curr_state = curr_conf.get('state', 'disabled') + if curr_state != req_state: + self._add_if_pfc_commands(if_name, req_state) + + def generate_commands(self): + req_interfaces = set() + for req_conf in self._required_config: + req_state = req_conf['state'] + if_name = req_conf['name'] + if req_state == 'enabled': + req_interfaces.add(if_name) + curr_conf = self._current_config.get(if_name, {}) + self._gen_pfc_commands(if_name, curr_conf, req_state) + if self._purge: + for if_name, curr_conf in iteritems(self._current_config): + if if_name not in req_interfaces: + req_state = 'disabled' + self._gen_pfc_commands(if_name, curr_conf, req_state) + + +def main(): + """ main entry point for module execution + """ + MlnxosPfcInterfaceModule.main() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/mlnxos/fixtures/mlnxos_pfc_interface_disabled.cfg b/test/units/modules/network/mlnxos/fixtures/mlnxos_pfc_interface_disabled.cfg new file mode 100644 index 00000000000..0d51d4d5c75 --- /dev/null +++ b/test/units/modules/network/mlnxos/fixtures/mlnxos_pfc_interface_disabled.cfg @@ -0,0 +1,26 @@ +{ + "Eth1/1": [ + { + "PFC admin": "Auto", + "PFC oper": "Disabled" + } + ], + "Eth1/1/2": [ + { + "PFC admin": "Auto", + "PFC oper": "Disabled" + } + ], + "Po1": [ + { + "PFC admin": "Auto", + "PFC oper": "Disabled" + } + ], + "Mpo2": [ + { + "PFC admin": "Auto", + "PFC oper": "Disabled" + } + ] +} diff --git a/test/units/modules/network/mlnxos/fixtures/mlnxos_pfc_interface_enabled.cfg b/test/units/modules/network/mlnxos/fixtures/mlnxos_pfc_interface_enabled.cfg new file mode 100644 index 00000000000..471edc8c657 --- /dev/null +++ b/test/units/modules/network/mlnxos/fixtures/mlnxos_pfc_interface_enabled.cfg @@ -0,0 +1,26 @@ +{ + "Eth1/1": [ + { + "PFC admin": "on", + "PFC oper": "Enabled" + } + ], + "Eth1/1/2": [ + { + "PFC admin": "on", + "PFC oper": "Enabled" + } + ], + "Po1": [ + { + "PFC admin": "on", + "PFC oper": "Enabled" + } + ], + "Mpo2": [ + { + "PFC admin": "on", + "PFC oper": "Enabled" + } + ] +} diff --git a/test/units/modules/network/mlnxos/test_mlnxos_pfc_interface.py b/test/units/modules/network/mlnxos/test_mlnxos_pfc_interface.py new file mode 100644 index 00000000000..d64fd5b0ffd --- /dev/null +++ b/test/units/modules/network/mlnxos/test_mlnxos_pfc_interface.py @@ -0,0 +1,109 @@ +# +# 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 ansible.compat.tests.mock import patch +from ansible.modules.network.mlnxos import mlnxos_pfc_interface +from units.modules.utils import set_module_args +from .mlnxos_module import TestMlnxosModule, load_fixture + + +class TestMlnxosPfcInterfaceModule(TestMlnxosModule): + + module = mlnxos_pfc_interface + + def setUp(self): + super(TestMlnxosPfcInterfaceModule, self).setUp() + self._pfc_enabled = True + self.mock_get_config = patch.object( + mlnxos_pfc_interface.MlnxosPfcInterfaceModule, + "_get_pfc_config") + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + 'ansible.module_utils.network.mlnxos.mlnxos.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + super(TestMlnxosPfcInterfaceModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None, transport='cli'): + if self._pfc_enabled: + suffix = 'enabled' + else: + suffix = 'disabled' + config_file = 'mlnxos_pfc_interface_%s.cfg' % suffix + + self.get_config.return_value = load_fixture(config_file) + self.load_config.return_value = None + + def _test_pfc_if(self, if_name, enabled, changed, commands): + state = 'enabled' if enabled else 'disabled' + set_module_args(dict(name=if_name, state=state)) + self.execute_module(changed=changed, commands=commands) + + def _test_pfc_no_change(self, enabled): + interfaces = ('Eth1/1', 'Eth1/1/2', 'Po1', 'Mpo2') + changed = False + commands = None + for ifc in interfaces: + self._test_pfc_if(ifc, enabled, changed, commands) + + def test_pfc_enabled_no_change(self): + self._pfc_enabled = True + enabled = True + self._test_pfc_no_change(enabled) + + def test_pfc_disabled_no_change(self): + self._pfc_enabled = False + enabled = False + self._test_pfc_no_change(enabled) + + def _test_pfc_change(self, enabled): + cmd_list = [ + ('Eth1/1', 'interface ethernet 1/1'), + ('Eth1/1/2', 'interface ethernet 1/1/2'), + ('Po1', 'interface port-channel 1'), + ('Mpo2', 'interface mlag-port-channel 2'), + ] + changed = True + suffix = ' dcb priority-flow-control mode on force' + if not enabled: + suffix = ' no dcb priority-flow-control mode force' + for (if_name, cmd) in cmd_list: + commands = [cmd + suffix] + self._test_pfc_if(if_name, enabled, changed, commands) + + def test_pfc_disabled_change(self): + self._pfc_enabled = False + enabled = True + self._test_pfc_change(enabled) + + def test_pfc_enabled_change(self): + self._pfc_enabled = True + enabled = False + self._test_pfc_change(enabled) + + def test_pfc_aggregate(self): + self._pfc_enabled = False + aggregate = [dict(name='Eth1/1'), dict(name='Eth1/1/2')] + set_module_args(dict(aggregate=aggregate, state='enabled')) + commands = [ + 'interface ethernet 1/1 dcb priority-flow-control mode on force', + 'interface ethernet 1/1/2 dcb priority-flow-control mode on force'] + self.execute_module(changed=True, commands=commands) + + def test_pfc_aggregate_purge(self): + self._pfc_enabled = True + aggregate = [dict(name='Po1'), dict(name='Mpo2')] + set_module_args(dict(aggregate=aggregate, state='enabled', purge=True)) + commands = [ + 'interface ethernet 1/1 no dcb priority-flow-control mode force', + 'interface ethernet 1/1/2 no dcb priority-flow-control mode force'] + self.execute_module(changed=True, commands=commands)