From 8ddb84a155a62cd6c71e1a70b7a7aa4fef1da5a7 Mon Sep 17 00:00:00 2001 From: Samer Deeb Date: Fri, 22 Dec 2017 22:41:25 -0800 Subject: [PATCH] Add module mlnxos_vlan for supporting vlan configuration on Mellanox switches (#34113) * Add module mlnxos_vlan for supporting vlan configuration on Mellanox switches Signed-off-by: Samer Deeb * use sgort header and fix METADATA Signed-off-by: Samer Deeb --- .../modules/network/mlnxos/mlnxos_vlan.py | 204 ++++++++++++++++++ .../mlnxos/fixtures/mlnxos_vlan_show.cfg | 14 ++ .../network/mlnxos/test_mlnxos_vlan.py | 103 +++++++++ 3 files changed, 321 insertions(+) create mode 100644 lib/ansible/modules/network/mlnxos/mlnxos_vlan.py create mode 100644 test/units/modules/network/mlnxos/fixtures/mlnxos_vlan_show.cfg create mode 100644 test/units/modules/network/mlnxos/test_mlnxos_vlan.py diff --git a/lib/ansible/modules/network/mlnxos/mlnxos_vlan.py b/lib/ansible/modules/network/mlnxos/mlnxos_vlan.py new file mode 100644 index 00000000000..fb68baff0ad --- /dev/null +++ b/lib/ansible/modules/network/mlnxos/mlnxos_vlan.py @@ -0,0 +1,204 @@ +#!/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_vlan +version_added: "2.5" +author: "Samer Deeb (@samerd) Alex Tabachnik (@atabachnik)" +short_description: Manage VLANs on Mellanox MLNX-OS network devices +description: + - This module provides declarative management of VLANs + on Mellanox MLNX-OS network devices. +options: + name: + description: + - Name of the VLAN. + vlan_id: + description: + - ID of the VLAN. + aggregate: + description: List of VLANs definitions. + purge: + description: + - Purge VLANs not defined in the I(aggregate) parameter. + default: no + state: + description: + - State of the VLAN configuration. + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +- name: configure VLAN ID and name + mlnxos_vlan: + vlan_id: 20 + name: test-vlan + +- name: remove configuration + mlnxos_vlan: + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always. + type: list + sample: + - vlan 20 + - name test-vlan + - exit +""" + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.common.utils import remove_default_spec + +from ansible.module_utils.network.mlnxos.mlnxos import BaseMlnxosModule +from ansible.module_utils.network.mlnxos.mlnxos import show_cmd + + +class MlnxosVlanModule(BaseMlnxosModule): + _purge = False + + @classmethod + def _get_element_spec(cls): + return dict( + vlan_id=dict(type='int'), + name=dict(type='str'), + state=dict(default='present', choices=['present', 'absent']), + ) + + @classmethod + def _get_aggregate_spec(cls, element_spec): + aggregate_spec = deepcopy(element_spec) + aggregate_spec['vlan_id'] = 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) + if aggregate_spec: + argument_spec = dict( + aggregate=dict(type='list', elements='dict', + options=aggregate_spec), + purge=dict(default=False, type='bool'), + ) + else: + argument_spec = dict() + argument_spec.update(element_spec) + required_one_of = [['vlan_id', 'aggregate']] + mutually_exclusive = [['vlan_id', 'aggregate']] + self._module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + def validate_vlan_id(self, value): + if value and not 1 <= int(value) <= 4094: + self._module.fail_json(msg='vlan id must be between 1 and 4094') + + 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() + req_item['vlan_id'] = int(req_item['vlan_id']) + self._required_config.append(req_item) + else: + params = { + 'vlan_id': module_params['vlan_id'], + 'name': module_params['name'], + 'state': module_params['state'], + } + self.validate_param_values(params) + self._required_config.append(params) + + def _create_vlan_data(self, vlan_id, vlan_data): + return { + 'vlan_id': vlan_id, + 'name': self.get_config_attr(vlan_data, 'Name') + } + + def _get_vlan_config(self): + return show_cmd(self._module, "show vlan") + + def load_current_config(self): + # called in base class in run function + self._current_config = dict() + vlan_config = self._get_vlan_config() + if not vlan_config: + return + for vlan_id, vlan_data in iteritems(vlan_config): + try: + vlan_id = int(vlan_id) + except ValueError: + continue + self._current_config[vlan_id] = \ + self._create_vlan_data(vlan_id, vlan_data) + + def generate_commands(self): + req_vlans = set() + for req_conf in self._required_config: + state = req_conf['state'] + vlan_id = req_conf['vlan_id'] + if state == 'absent': + if vlan_id in self._current_config: + self._commands.append('no vlan %s' % vlan_id) + else: + req_vlans.add(vlan_id) + self._generate_vlan_commands(vlan_id, req_conf) + if self._purge: + for vlan_id in self._current_config: + if vlan_id not in req_vlans: + self._commands.append('no vlan %s' % vlan_id) + + def _generate_vlan_commands(self, vlan_id, req_conf): + curr_vlan = self._current_config.get(vlan_id, {}) + if not curr_vlan: + self._commands.append("vlan %s" % vlan_id) + self._commands.append("exit") + req_name = req_conf['name'] + curr_name = curr_vlan.get('name') + if req_name: + if req_name != curr_name: + self._commands.append("vlan %s name %s" % (vlan_id, req_name)) + elif req_name is not None: + if curr_name: + self._commands.append("vlan %s no name" % vlan_id) + + +def main(): + """ main entry point for module execution + """ + MlnxosVlanModule.main() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/mlnxos/fixtures/mlnxos_vlan_show.cfg b/test/units/modules/network/mlnxos/fixtures/mlnxos_vlan_show.cfg new file mode 100644 index 00000000000..d1ae46bf48b --- /dev/null +++ b/test/units/modules/network/mlnxos/fixtures/mlnxos_vlan_show.cfg @@ -0,0 +1,14 @@ +{ + "1": { + "Name": "default", + "Ports": "Eth1/1, Eth1/2, Eth1/3, Eth1/4, Eth1/5,\nEth1/6, Eth1/7, Eth1/8, Eth1/9, Eth1/10,\nEth1/11, Eth1/12, Eth1/13, Eth1/14, Eth1/15,\nEth1/16" + }, + "10": { + "Name": "test 10", + "Ports": "" + }, + "20": { + "Name": "test 20", + "Ports": "" + } +} diff --git a/test/units/modules/network/mlnxos/test_mlnxos_vlan.py b/test/units/modules/network/mlnxos/test_mlnxos_vlan.py new file mode 100644 index 00000000000..c9ac02aa407 --- /dev/null +++ b/test/units/modules/network/mlnxos/test_mlnxos_vlan.py @@ -0,0 +1,103 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import json + +from ansible.compat.tests.mock import patch +from ansible.modules.network.mlnxos import mlnxos_vlan +from ansible.module_utils.network.mlnxos import mlnxos as mlnxos_utils +from units.modules.utils import set_module_args +from .mlnxos_module import TestMlnxosModule, load_fixture + + +class TestMlnxosVlanModule(TestMlnxosModule): + + module = mlnxos_vlan + + def setUp(self): + super(TestMlnxosVlanModule, self).setUp() + self.mock_get_config = patch.object( + mlnxos_vlan.MlnxosVlanModule, "_get_vlan_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(TestMlnxosVlanModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + + def load_fixtures(self, commands=None, transport='cli'): + config_file = 'mlnxos_vlan_show.cfg' + self.get_config.return_value = load_fixture(config_file) + self.load_config.return_value = None + + def test_vlan_no_change(self): + set_module_args(dict(vlan_id=20)) + self.execute_module(changed=False) + + def test_vlan_remove_name(self): + set_module_args(dict(vlan_id=10, name='')) + commands = ['vlan 10 no name'] + self.execute_module(changed=True, commands=commands) + + def test_vlan_change_name(self): + set_module_args(dict(vlan_id=10, name='test-test')) + commands = ['vlan 10 name test-test'] + self.execute_module(changed=True, commands=commands) + + def test_vlan_create(self): + set_module_args(dict(vlan_id=30)) + commands = ['vlan 30', 'exit'] + self.execute_module(changed=True, commands=commands) + + def test_vlan_create_with_name(self): + set_module_args(dict(vlan_id=30, name='test-test')) + commands = ['vlan 30', 'exit', 'vlan 30 name test-test'] + self.execute_module(changed=True, commands=commands) + + def test_vlan_remove(self): + set_module_args(dict(vlan_id=20, state='absent')) + commands = ['no vlan 20'] + self.execute_module(changed=True, commands=commands) + + def test_vlan_remove_not_exist(self): + set_module_args(dict(vlan_id=30, state='absent')) + self.execute_module(changed=False) + + def test_vlan_aggregate(self): + aggregate = list() + aggregate.append(dict(vlan_id=30)) + aggregate.append(dict(vlan_id=20)) + set_module_args(dict(aggregate=aggregate)) + commands = ['vlan 30', 'exit'] + self.execute_module(changed=True, commands=commands) + + def test_vlan_aggregate_purge(self): + aggregate = list() + aggregate.append(dict(vlan_id=30)) + aggregate.append(dict(vlan_id=20)) + set_module_args(dict(aggregate=aggregate, purge=True)) + commands = ['vlan 30', 'exit', 'no vlan 10', 'no vlan 1'] + self.execute_module(changed=True, commands=commands)