From fcf6da0a9df6c8be6c1913d5f89d5cca40602cd8 Mon Sep 17 00:00:00 2001 From: Samer Deeb Date: Thu, 11 Jan 2018 09:40:04 -0800 Subject: [PATCH] Add ospf protocol support for Mellanox MLNXOS network devices (#34717) * Add new module for managing ospf protocol on mlnxos devices Signed-off-by: Samer Deeb * Fix test name, and documentation. Signed-off-by: Samer Deeb --- .../modules/network/mlnxos/mlnxos_ospf.py | 238 ++++++++++++++++++ .../fixtures/mlnxos_ospf_interfaces_show.cfg | 5 + .../mlnxos/fixtures/mlnxos_ospf_show.cfg | 1 + .../network/mlnxos/test_mlnxos_ospf.py | 106 ++++++++ 4 files changed, 350 insertions(+) create mode 100644 lib/ansible/modules/network/mlnxos/mlnxos_ospf.py create mode 100644 test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_interfaces_show.cfg create mode 100644 test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_show.cfg create mode 100644 test/units/modules/network/mlnxos/test_mlnxos_ospf.py diff --git a/lib/ansible/modules/network/mlnxos/mlnxos_ospf.py b/lib/ansible/modules/network/mlnxos/mlnxos_ospf.py new file mode 100644 index 0000000000..ca7f526cc3 --- /dev/null +++ b/lib/ansible/modules/network/mlnxos/mlnxos_ospf.py @@ -0,0 +1,238 @@ +#!/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_ospf +version_added: "2.5" +author: "Samer Deeb (@samerd)" +short_description: Manage OSPF protocol on Mellanox MLNX-OS network devices +description: + - This module provides declarative management and configuration of OSPF + protocol on Mellanox MLNX-OS network devices. +notes: + - Tested on MLNX-OS 3.6.4000 +options: + ospf: + description: + - "OSPF instance number 1-65535" + required: true + router_id: + description: + - OSPF router ID. Required if I(state=present). + interfaces: + description: + - List of interfaces and areas. Required if I(state=present). + suboptions: + name: + description: + - Intrface name. + required: true + area: + description: + - OSPF area. + required: true + state: + description: + - OSPF state. + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +- name: add ospf router to interface + mlnxos_ospf: + ospf: 2 + router_id: 192.168.8.2 + interfaces: + - name: Eth1/1 + - area: 0.0.0.0 +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device. + returned: always + type: list + sample: + - router ospf 2 + - router-id 192.168.8.2 + - exit + - interface ethernet 1/1 ip ospf area 0.0.0.0 +""" +import re + +from ansible.module_utils.basic import AnsibleModule +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 MlnxosOspfModule(BaseMlnxosModule): + OSPF_IF_REGEX = re.compile( + r'^(Loopback\d+|Eth\d+\/\d+|Vlan\d+|Po\d+)\s+(\S+).*') + OSPF_ROUTER_REGEX = re.compile(r'^Routing Process (\d+).*ID\s+(\S+).*') + + @classmethod + def _get_element_spec(cls): + interface_spec = dict( + name=dict(required=True), + area=dict(required=True), + ) + element_spec = dict( + ospf=dict(type='int', required=True), + router_id=dict(), + interfaces=dict(type='list', elements='dict', + options=interface_spec), + state=dict(choices=['present', 'absent'], default='present'), + ) + return element_spec + + def init_module(self): + """ Ansible module initialization + """ + 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 validate_ospf(self, value): + if value and not 1 <= int(value) <= 65535: + self._module.fail_json(msg='ospf id must be between 1 and 65535') + + def get_required_config(self): + module_params = self._module.params + self._required_config = dict( + ospf=module_params['ospf'], + router_id=module_params['router_id'], + state=module_params['state'], + ) + interfaces = module_params['interfaces'] or list() + req_interfaces = self._required_config['interfaces'] = dict() + for interface_data in interfaces: + req_interfaces[interface_data['name']] = interface_data['area'] + self.validate_param_values(self._required_config) + + def _update_ospf_data(self, ospf_data): + match = self.OSPF_ROUTER_REGEX.match(ospf_data) + if match: + ospf_id = int(match.group(1)) + router_id = match.group(2) + self._current_config['ospf'] = ospf_id + self._current_config['router_id'] = router_id + + def _update_ospf_interfaces(self, ospf_interfaces): + interfaces = self._current_config['interfaces'] = dict() + lines = ospf_interfaces.split('\n') + for line in lines: + line = line.strip() + match = self.OSPF_IF_REGEX.match(line) + if match: + name = match.group(1) + area = match.group(2) + for prefix in ("Vlan", "Loopback"): + if name.startswith(prefix): + name = name.replace(prefix, prefix + ' ') + interfaces[name] = area + + def _get_ospf_config(self, ospf_id): + cmd = 'show ip ospf %s | include Process' % ospf_id + return show_cmd(self._module, cmd, json_fmt=False, fail_on_error=False) + + def _get_ospf_interfaces_config(self, ospf_id): + cmd = 'show ip ospf interface %s brief' % ospf_id + return show_cmd(self._module, cmd, json_fmt=False, fail_on_error=False) + + def load_current_config(self): + # called in base class in run function + ospf_id = self._required_config['ospf'] + self._current_config = dict() + ospf_data = self._get_ospf_config(ospf_id) + if ospf_data: + self._update_ospf_data(ospf_data) + ospf_interfaces = self._get_ospf_interfaces_config(ospf_id) + if ospf_interfaces: + self._update_ospf_interfaces(ospf_interfaces) + + def _generate_no_ospf_commands(self): + req_ospf_id = self._required_config['ospf'] + curr_ospf_id = self._current_config.get('ospf') + if curr_ospf_id == req_ospf_id: + cmd = 'no router ospf %s' % req_ospf_id + self._commands.append(cmd) + + def _get_interface_command_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('Vlan'): + return if_name.replace("Vlan", "vlan") + if if_name.startswith('Loopback'): + return if_name.replace("Loopback", "loopback") + self._module.fail_json( + msg='invalid interface name: %s' % if_name) + + def _get_interface_area_cmd(self, if_name, area): + interface_prefix = self._get_interface_command_name(if_name) + if area: + area_cmd = 'ip ospf area %s' % area + else: + area_cmd = 'no ip ospf area' + cmd = 'interface %s %s' % (interface_prefix, area_cmd) + return cmd + + def _generate_ospf_commands(self): + req_router_id = self._required_config['router_id'] + req_ospf_id = self._required_config['ospf'] + curr_router_id = self._current_config.get('router_id') + curr_ospf_id = self._current_config.get('ospf') + if curr_ospf_id != req_ospf_id or req_router_id != curr_router_id: + cmd = 'router ospf %s' % req_ospf_id + self._commands.append(cmd) + if req_router_id != curr_router_id: + if req_router_id: + cmd = 'router-id %s' % req_router_id + else: + cmd = 'no router-id' + self._commands.append(cmd) + self._commands.append('exit') + req_interfaces = self._required_config['interfaces'] + curr_interfaces = self._current_config.get('interfaces', dict()) + for if_name, area in iteritems(req_interfaces): + curr_area = curr_interfaces.get(if_name) + if curr_area != area: + cmd = self._get_interface_area_cmd(if_name, area) + self._commands.append(cmd) + for if_name in curr_interfaces: + if if_name not in req_interfaces: + cmd = self._get_interface_area_cmd(if_name, None) + self._commands.append(cmd) + + def generate_commands(self): + req_state = self._required_config['state'] + if req_state == 'absent': + return self._generate_no_ospf_commands() + return self._generate_ospf_commands() + + +def main(): + """ main entry point for module execution + """ + MlnxosOspfModule.main() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_interfaces_show.cfg b/test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_interfaces_show.cfg new file mode 100644 index 0000000000..7566305862 --- /dev/null +++ b/test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_interfaces_show.cfg @@ -0,0 +1,5 @@ + +OSPF Process ID 2 VRF default +Total number of interface: 1 +Interface Id Area Cost State Neighbors Status +Loopback1 0.0.0.0 1 Enabled 0 Up diff --git a/test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_show.cfg b/test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_show.cfg new file mode 100644 index 0000000000..72e385684b --- /dev/null +++ b/test/units/modules/network/mlnxos/fixtures/mlnxos_ospf_show.cfg @@ -0,0 +1 @@ +Routing Process 2 with ID 10.2.3.4 default diff --git a/test/units/modules/network/mlnxos/test_mlnxos_ospf.py b/test/units/modules/network/mlnxos/test_mlnxos_ospf.py new file mode 100644 index 0000000000..7383e2817f --- /dev/null +++ b/test/units/modules/network/mlnxos/test_mlnxos_ospf.py @@ -0,0 +1,106 @@ +# +# 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_ospf +from units.modules.utils import set_module_args +from .mlnxos_module import TestMlnxosModule, load_fixture + + +class TestMlnxosOspfModule(TestMlnxosModule): + + module = mlnxos_ospf + + def setUp(self): + super(TestMlnxosOspfModule, self).setUp() + self._ospf_exists = True + self.mock_get_config = patch.object( + mlnxos_ospf.MlnxosOspfModule, + "_get_ospf_config") + self.get_config = self.mock_get_config.start() + + self.mock_get_interfaces_config = patch.object( + mlnxos_ospf.MlnxosOspfModule, + "_get_ospf_interfaces_config") + self.get_interfaces_config = self.mock_get_interfaces_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(TestMlnxosOspfModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None, transport='cli'): + if self._ospf_exists: + config_file = 'mlnxos_ospf_show.cfg' + self.get_config.return_value = load_fixture(config_file) + config_file = 'mlnxos_ospf_interfaces_show.cfg' + self.get_interfaces_config.return_value = load_fixture(config_file) + else: + self.get_config.return_value = None + self.get_interfaces_config.return_value = None + self.load_config.return_value = None + + def test_ospf_absent_no_change(self): + set_module_args(dict(ospf=3, state='absent')) + self.execute_module(changed=False) + + def test_ospf_present_no_change(self): + interface = dict(name='Loopback 1', area='0.0.0.0') + set_module_args(dict(ospf=2, router_id='10.2.3.4', + interfaces=[interface])) + self.execute_module(changed=False) + + def test_ospf_present_remove(self): + set_module_args(dict(ospf=2, state='absent')) + commands = ['no router ospf 2'] + self.execute_module(changed=True, commands=commands) + + def test_ospf_change_router(self): + interface = dict(name='Loopback 1', area='0.0.0.0') + set_module_args(dict(ospf=2, router_id='10.2.3.5', + interfaces=[interface])) + commands = ['router ospf 2', 'router-id 10.2.3.5', 'exit'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_ospf_remove_router(self): + interface = dict(name='Loopback 1', area='0.0.0.0') + set_module_args(dict(ospf=2, interfaces=[interface])) + commands = ['router ospf 2', 'no router-id', 'exit'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_ospf_add_interface(self): + interfaces = [dict(name='Loopback 1', area='0.0.0.0'), + dict(name='Loopback 2', area='0.0.0.0')] + set_module_args(dict(ospf=2, router_id='10.2.3.4', + interfaces=interfaces)) + commands = ['interface loopback 2 ip ospf area 0.0.0.0'] + self.execute_module(changed=True, commands=commands) + + def test_ospf_remove_interface(self): + set_module_args(dict(ospf=2, router_id='10.2.3.4')) + commands = ['interface loopback 1 no ip ospf area'] + self.execute_module(changed=True, commands=commands) + + def test_ospf_add(self): + self._ospf_exists = False + interfaces = [dict(name='Loopback 1', area='0.0.0.0'), + dict(name='Vlan 210', area='0.0.0.0'), + dict(name='Eth1/1', area='0.0.0.0'), + dict(name='Po1', area='0.0.0.0')] + set_module_args(dict(ospf=2, router_id='10.2.3.4', + interfaces=interfaces)) + commands = ['router ospf 2', 'router-id 10.2.3.4', 'exit', + 'interface loopback 1 ip ospf area 0.0.0.0', + 'interface vlan 210 ip ospf area 0.0.0.0', + 'interface ethernet 1/1 ip ospf area 0.0.0.0', + 'interface port-channel 1 ip ospf area 0.0.0.0'] + self.execute_module(changed=True, commands=commands)