diff --git a/lib/ansible/modules/network/mlnxos/mlnxos_mlag_ipl.py b/lib/ansible/modules/network/mlnxos/mlnxos_mlag_ipl.py new file mode 100644 index 00000000000..333708cdeaa --- /dev/null +++ b/lib/ansible/modules/network/mlnxos/mlnxos_mlag_ipl.py @@ -0,0 +1,210 @@ +#!/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_mlag_ipl +version_added: "2.5" +author: "Samer Deeb (@samerd)" +short_description: Manage IPL (inter-peer link) on MLNX-OS network devices +description: + - This module provides declarative management of IPL (inter-peer link) + management on MLNX-OS network devices. +notes: + - tested on Mellanox OS 3.6.4000 +options: + name: + description: + - Name of the interface (port-channel) IPL should be configured on. + required: true + vlan_interface: + description: + - Name of the IPL vlan interface. + state: + description: + - IPL state + default: present + choices: ['present', 'absent'] + peer_address: + description: + - IPL peer IP address +""" + +EXAMPLES = """ +- name: run configure ipl + mlnxos_mlag_ipl: + name: Po1 + vlan_interface: Vlan 322 + state: present + peer_address: 192.168.7.1 + +- name: run remove ipl + mlnxos_mlag_ipl: + name: Po1 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device. + returned: always + type: list + sample: + - interface port-channel 1 ipl 1 + - interface vlan 1024 ipl 1 peer-address 10.10.10.10 +""" +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible.module_utils.network.mlnxos.mlnxos import BaseMlnxosModule +from ansible.module_utils.network.mlnxos.mlnxos import show_cmd + + +class MlnxosMlagIplModule(BaseMlnxosModule): + VLAN_IF_REGEX = re.compile(r'^Vlan \d+') + + @classmethod + def _get_element_spec(cls): + return dict( + name=dict(required=True), + state=dict(default='present', + choices=['present', 'absent']), + peer_address=dict(), + vlan_interface=dict(), + ) + + def init_module(self): + """ main entry point for module execution + """ + element_spec = self._get_element_spec() + 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( + name=module_params['name'], + state=module_params['state'], + peer_address=module_params['peer_address'], + vlan_interface=module_params['vlan_interface']) + self.validate_param_values(self._required_config) + + def _update_mlag_data(self, mlag_data): + if not mlag_data: + return + mlag_summary = mlag_data.get("MLAG IPLs Summary", {}) + ipl_id = "1" + ipl_list = mlag_summary.get(ipl_id) + if ipl_list: + ipl_data = ipl_list[0] + vlan_id = ipl_data.get("Vlan Interface") + vlan_interface = "" + if vlan_id != "N/A": + vlan_interface = "Vlan %s" % vlan_id + peer_address = ipl_data.get("Peer IP address") + name = ipl_data.get("Group Port-Channel") + self._current_config = dict( + name=name, + peer_address=peer_address, + vlan_interface=vlan_interface) + + def _show_mlag_data(self): + cmd = "show mlag" + return show_cmd(self._module, cmd, json_fmt=True, fail_on_error=False) + + def load_current_config(self): + # called in base class in run function + self._current_config = dict() + mlag_data = self._show_mlag_data() + self._update_mlag_data(mlag_data) + + def _get_interface_cmd_name(self, if_name): + if if_name.startswith('Po'): + return if_name.replace("Po", "port-channel ") + self._module.fail_json( + msg='invalid interface name: %s' % if_name) + + def _generate_port_channel_command(self, if_name, enable): + if_cmd_name = self._get_interface_cmd_name(if_name) + if enable: + ipl_cmd = 'ipl 1' + else: + ipl_cmd = "no ipl 1" + cmd = "interface %s %s" % (if_cmd_name, ipl_cmd) + return cmd + + def _generate_vlan_if_command(self, if_name, enable, peer_address): + if_cmd_name = if_name.lower() + if enable: + ipl_cmd = 'ipl 1 peer-address %s' % peer_address + else: + ipl_cmd = "no ipl 1" + cmd = "interface %s %s" % (if_cmd_name, ipl_cmd) + return cmd + + def _generate_no_ipl_commands(self): + curr_interface = self._current_config.get('name') + req_interface = self._required_config.get('name') + if curr_interface == req_interface: + cmd = self._generate_port_channel_command( + req_interface, enable=False) + self._commands.append(cmd) + + def _generate_ipl_commands(self): + curr_interface = self._current_config.get('name') + req_interface = self._required_config.get('name') + if curr_interface != req_interface: + if curr_interface and curr_interface != 'N/A': + cmd = self._generate_port_channel_command( + curr_interface, enable=False) + self._commands.append(cmd) + cmd = self._generate_port_channel_command( + req_interface, enable=True) + self._commands.append(cmd) + curr_vlan = self._current_config.get('vlan_interface') + req_vlan = self._required_config.get('vlan_interface') + add_peer = False + if curr_vlan != req_vlan: + add_peer = True + if curr_vlan: + cmd = self._generate_vlan_if_command(curr_vlan, enable=False, + peer_address=None) + self._commands.append(cmd) + curr_peer = self._current_config.get('peer_address') + req_peer = self._required_config.get('peer_address') + if req_peer != curr_peer: + add_peer = True + if add_peer and req_peer: + cmd = self._generate_vlan_if_command(req_vlan, enable=True, + peer_address=req_peer) + self._commands.append(cmd) + + def generate_commands(self): + state = self._required_config['state'] + if state == 'absent': + self._generate_no_ipl_commands() + else: + self._generate_ipl_commands() + + +def main(): + """ main entry point for module execution + """ + MlnxosMlagIplModule.main() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/mlnxos/fixtures/mlnxos_mlag_ipl_show.cfg b/test/units/modules/network/mlnxos/fixtures/mlnxos_mlag_ipl_show.cfg new file mode 100644 index 00000000000..6d1f3df5779 --- /dev/null +++ b/test/units/modules/network/mlnxos/fixtures/mlnxos_mlag_ipl_show.cfg @@ -0,0 +1,29 @@ +{ + "Reload-delay": "30 sec", + "Upgrade-timeout": "60 min", + "System-mac": "00:00:5E:00:01:4E [Mismatched]", + "Admin status": "Disabled", + "MLAG Ports Status Summary": { + "Active-partial": "0", + "Inactive": "0", + "Active-full": "0" + }, + "MLAG IPLs Summary": { + "1": [ + { + "Local IP address": "10.2.2.3", + "Peer IP address": "10.2.2.2", + "Operational State": "Down", + "Vlan Interface": "1002", + "Group Port-Channel": "Po1" + } + ] + }, + "Keepalive-interval": "1 sec", + "MLAG Ports Configuration Summary": { + "Disabled": "0", + "Configured": "0", + "Enabled": "0" + }, + "Operational status": "Down" +} diff --git a/test/units/modules/network/mlnxos/test_mlnxos_mlag_ipl.py b/test/units/modules/network/mlnxos/test_mlnxos_mlag_ipl.py new file mode 100644 index 00000000000..0a18980b531 --- /dev/null +++ b/test/units/modules/network/mlnxos/test_mlnxos_mlag_ipl.py @@ -0,0 +1,86 @@ +# +# 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_mlag_ipl +from units.modules.utils import set_module_args +from .mlnxos_module import TestMlnxosModule, load_fixture + + +class TestMlnxosMlagIplModule(TestMlnxosModule): + + module = mlnxos_mlag_ipl + + def setUp(self): + super(TestMlnxosMlagIplModule, self).setUp() + self._mlag_enabled = True + self.mock_get_config = patch.object( + mlnxos_mlag_ipl.MlnxosMlagIplModule, + "_show_mlag_data") + 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(TestMlnxosMlagIplModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None, transport='cli'): + if self._mlag_enabled: + config_file = 'mlnxos_mlag_ipl_show.cfg' + self.get_config.return_value = load_fixture(config_file) + else: + self.get_config.return_value = None + self.load_config.return_value = None + + def test_no_ipl_no_change(self): + self._mlag_enabled = False + set_module_args(dict(name="Po1", state='absent')) + self.execute_module(changed=False) + + def test_ipl_no_change(self): + self._mlag_enabled = True + set_module_args(dict(name="Po1", state='present', + vlan_interface='Vlan 1002', + peer_address='10.2.2.2')) + self.execute_module(changed=False) + + def test_ipl_add(self): + self._mlag_enabled = False + set_module_args(dict(name="Po1", state='present', + vlan_interface='Vlan 1002', + peer_address='10.2.2.2')) + commands = ['interface port-channel 1 ipl 1', + 'interface vlan 1002 ipl 1 peer-address 10.2.2.2'] + self.execute_module(changed=True, commands=commands) + + def test_ipl_add_peer(self): + self._mlag_enabled = True + set_module_args(dict(name="Po1", state='present', + vlan_interface='Vlan 1002', + peer_address='10.2.2.4')) + commands = ['interface vlan 1002 ipl 1 peer-address 10.2.2.4'] + self.execute_module(changed=True, commands=commands) + + def test_ipl_remove(self): + self._mlag_enabled = True + set_module_args(dict(name="Po1", state='absent')) + commands = ['interface port-channel 1 no ipl 1'] + self.execute_module(changed=True, commands=commands) + + def test_ipl_change_vlan(self): + self._mlag_enabled = True + set_module_args(dict(name="Po1", state='present', + vlan_interface='Vlan 1003', + peer_address='10.2.2.4')) + commands = ['interface vlan 1002 no ipl 1', + 'interface vlan 1003 ipl 1 peer-address 10.2.2.4'] + self.execute_module(changed=True, commands=commands)