From 7e1a347695c7987ae56ef1b6919156d9254010ad Mon Sep 17 00:00:00 2001 From: sushma-alethea <52454757+sushma-alethea@users.noreply.github.com> Date: Mon, 26 Aug 2019 09:12:59 +0530 Subject: [PATCH] icx: new module icx_linkagg (#59967) * new module * new module * new terminal * new terminal * new cliconf * new cliconf * cliconf * cliconf * icx cliconf * icx cliconf * icx_cliconf * icx test units module * icx test units module * icx units module * icx units module * icx banner unit test * icx banner unit test * PR changes resolved * changes resolved * Changes Resolved * check_running_config changes resolved * added notes * added notes * removed icx rst * new changes * new changes * deleted icx rst * icx .rst * icx .rst * modified platform_index.rst * modified platform_index.rst * modified platform_index.rst * modified platform_index.rst * changes resolved * changes resolved * PR comments resolved * PR comments resolved * Update platform_index.rst PR comment resolved * Update platform_index.rst PR comment resolved * new module icx_linkagg * bug fixes * Fixing bot errors * Trying to fix aggregate document error * Added options under aggregate in documentation * Fixed bot error * Fixed documentation error * Fix bot error * notes updated --- .../modules/network/icx/icx_linkagg.py | 328 ++++++++++++++++++ .../icx/fixtures/lag_running_config.txt | 7 + .../modules/network/icx/test_icx_linkagg.py | 123 +++++++ 3 files changed, 458 insertions(+) create mode 100644 lib/ansible/modules/network/icx/icx_linkagg.py create mode 100644 test/units/modules/network/icx/fixtures/lag_running_config.txt create mode 100644 test/units/modules/network/icx/test_icx_linkagg.py diff --git a/lib/ansible/modules/network/icx/icx_linkagg.py b/lib/ansible/modules/network/icx/icx_linkagg.py new file mode 100644 index 00000000000..f2dc57c0c07 --- /dev/null +++ b/lib/ansible/modules/network/icx/icx_linkagg.py @@ -0,0 +1,328 @@ +#!/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: icx_linkagg +version_added: "2.9" +author: "Ruckus Wireless (@Commscope)" +short_description: Manage link aggregation groups on Ruckus ICX 7000 series switches +description: + - This module provides declarative management of link aggregation groups + on Ruckus ICX network devices. +notes: + - Tested against ICX 10.1. + - For information on using ICX platform, see L(the ICX OS Platform Options guide,../network/user_guide/platform_icx.html). +options: + group: + description: + - Channel-group number for the port-channel + Link aggregation group. Range 1-255 or set to 'auto' to auto-generates a LAG ID + type: int + name: + description: + - Name of the LAG + type: str + mode: + description: + - Mode of the link aggregation group. + type: str + choices: ['dynamic', 'static'] + members: + description: + - List of port members or ranges of the link aggregation group. + type: list + state: + description: + - State of the link aggregation group. + type: str + default: present + choices: ['present', 'absent'] + check_running_config: + description: + - Check running configuration. This can be set as environment variable. + Module will use environment variable value(default:True), unless it is overriden, by specifying it as module parameter. + type: bool + default: yes + aggregate: + description: + - List of link aggregation definitions. + type: list + suboptions: + group: + description: + - Channel-group number for the port-channel + Link aggregation group. Range 1-255 or set to 'auto' to auto-generates a LAG ID + type: int + name: + description: + - Name of the LAG + type: str + mode: + description: + - Mode of the link aggregation group. + type: str + choices: ['dynamic', 'static'] + members: + description: + - List of port members or ranges of the link aggregation group. + type: list + state: + description: + - State of the link aggregation group. + type: str + choices: ['present', 'absent'] + check_running_config: + description: + - Check running configuration. This can be set as environment variable. + Module will use environment variable value(default:True), unless it is overriden, by specifying it as module parameter. + type: bool + purge: + description: + - Purge links not defined in the I(aggregate) parameter. + type: bool + default: no + +""" + +EXAMPLES = """ +- name: create static link aggregation group + icx_linkagg: + group: 10 + mode: static + name: LAG1 + +- name: create link aggregation group with auto id + icx_linkagg: + group: auto + mode: dynamic + name: LAG2 + +- name: delete link aggregation group + icx_linkagg: + group: 10 + state: absent + +- name: Set members to LAG + icx_linkagg: + group: 200 + mode: static + members: + - ethernet 1/1/1 to 1/1/6 + - ethernet 1/1/10 + +- name: Remove links other then LAG id 100 and 3 using purge + icx_linkagg: + aggregate: + - { group: 3} + - { group: 100} + purge: true +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - lag LAG1 dynamic id 11 + - ports ethernet 1/1/1 to 1/1/6 + - no ports ethernet 1/1/10 + - no lag LAG1 dynamic id 12 +""" + + +import re +from copy import deepcopy + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.connection import ConnectionError, exec_command +from ansible.module_utils.network.icx.icx import run_commands, get_config, load_config +from ansible.module_utils.network.common.config import CustomNetworkConfig +from ansible.module_utils.network.common.utils import remove_default_spec + + +def range_to_members(ranges, prefix=""): + match = re.findall(r'(ethe[a-z]* [0-9]/[0-9]/[0-9]+)( to [0-9]/[0-9]/[0-9]+)?', ranges) + members = list() + for m in match: + start, end = m + if(end == ''): + start = start.replace("ethe ", "ethernet ") + members.append("%s%s" % (prefix, start)) + else: + start_tmp = re.search(r'[0-9]/[0-9]/([0-9]+)', start) + end_tmp = re.search(r'[0-9]/[0-9]/([0-9]+)', end) + start = int(start_tmp.group(1)) + end = int(end_tmp.group(1)) + 1 + for num in range(start, end): + members.append("%sethernet 1/1/%s" % (prefix, num)) + return members + + +def map_config_to_obj(module): + objs = dict() + compare = module.params['check_running_config'] + config = get_config(module, None, compare=compare) + obj = None + for line in config.split('\n'): + l = line.strip() + match1 = re.search(r'lag (\S+) (\S+) id (\S+)', l, re.M) + if match1: + obj = dict() + obj['name'] = match1.group(1) + obj['mode'] = match1.group(2) + obj['group'] = match1.group(3) + obj['state'] = 'present' + obj['members'] = list() + else: + match2 = re.search(r'ports .*', l, re.M) + if match2 and obj is not None: + obj['members'].extend(range_to_members(match2.group(0))) + elif obj is not None: + objs[obj['group']] = obj + obj = None + return objs + + +def map_params_to_obj(module): + obj = [] + + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + d = item.copy() + d['group'] = str(d['group']) + obj.append(d) + else: + obj.append({ + 'group': str(module.params['group']), + 'mode': module.params['mode'], + 'members': module.params['members'], + 'state': module.params['state'], + 'name': module.params['name'] + }) + + return obj + + +def search_obj_in_list(group, lst): + for o in lst: + if o['group'] == group: + return o + return None + + +def is_member(member, lst): + for li in lst: + ml = range_to_members(li) + if member in ml: + return True + return False + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + purge = module.params['purge'] + + for w in want: + if have == {} and w['state'] == 'absent': + commands.append("%slag %s %s id %s" % ('no ' if w['state'] == 'absent' else '', w['name'], w['mode'], w['group'])) + elif have.get(w['group']) is None: + commands.append("%slag %s %s id %s" % ('no ' if w['state'] == 'absent' else '', w['name'], w['mode'], w['group'])) + if(w.get('members') is not None and w['state'] == 'present'): + for m in w['members']: + commands.append("ports %s" % (m)) + if w['state'] == 'present': + commands.append("exit") + else: + commands.append("%slag %s %s id %s" % ('no ' if w['state'] == 'absent' else '', w['name'], w['mode'], w['group'])) + if(w.get('members') is not None and w['state'] == 'present'): + for m in have[w['group']]['members']: + if not is_member(m, w['members']): + commands.append("no ports %s" % (m)) + for m in w['members']: + sm = range_to_members(ranges=m) + for smm in sm: + if smm not in have[w['group']]['members']: + commands.append("ports %s" % (smm)) + + if w['state'] == 'present': + commands.append("exit") + if purge: + for h in have: + if search_obj_in_list(have[h]['group'], want) is None: + commands.append("no lag %s %s id %s" % (have[h]['name'], have[h]['mode'], have[h]['group'])) + return commands + + +def main(): + element_spec = dict( + group=dict(type='int'), + name=dict(type='str'), + mode=dict(choices=['dynamic', 'static']), + members=dict(type='list'), + state=dict(default='present', + choices=['present', 'absent']), + check_running_config=dict(default=True, type='bool', fallback=(env_fallback, ['ANSIBLE_CHECK_ICX_RUNNING_CONFIG'])) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['group'] = dict(required=True, type='int') + + required_one_of = [['group', 'aggregate']] + required_together = [['name', 'group']] + mutually_exclusive = [['group', 'aggregate']] + + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec, required_together=required_together), + purge=dict(default=False, type='bool') + ) + + argument_spec.update(element_spec) + + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + required_together=required_together, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + result = {'changed': False} + exec_command(module, 'skip') + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + commands = map_obj_to_commands((want, have), module) + + result["commands"] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/icx/fixtures/lag_running_config.txt b/test/units/modules/network/icx/fixtures/lag_running_config.txt new file mode 100644 index 00000000000..fdec5106ffd --- /dev/null +++ b/test/units/modules/network/icx/fixtures/lag_running_config.txt @@ -0,0 +1,7 @@ +lag LAG1 dynamic id 100 + ports ethe 1/1/3 ethe 1/1/5 to 1/1/8 + disable ethe 1/1/3 ethe 1/1/5 to 1/1/8 +! +lag LAG2 dynamic id 200 + ports ethe 1/1/11 ethe 1/1/13 ethe 1/1/15 + disable ethe 1/1/11 ethe 1/1/13 ethe 1/1/15 \ No newline at end of file diff --git a/test/units/modules/network/icx/test_icx_linkagg.py b/test/units/modules/network/icx/test_icx_linkagg.py new file mode 100644 index 00000000000..17580e75646 --- /dev/null +++ b/test/units/modules/network/icx/test_icx_linkagg.py @@ -0,0 +1,123 @@ +# Copyright: (c) 2019, 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 + +from units.compat.mock import patch +from ansible.modules.network.icx import icx_linkagg +from units.modules.utils import set_module_args +from .icx_module import TestICXModule, load_fixture + + +class TestICXLinkaggModule(TestICXModule): + + module = icx_linkagg + + def setUp(self): + super(TestICXLinkaggModule, self).setUp() + self.mock_get_config = patch('ansible.modules.network.icx.icx_linkagg.get_config') + self.get_config = self.mock_get_config.start() + self.mock_load_config = patch('ansible.modules.network.icx.icx_linkagg.load_config') + self.load_config = self.mock_load_config.start() + self.mock_exec_command = patch('ansible.modules.network.icx.icx_linkagg.exec_command') + self.exec_command = self.mock_exec_command.start() + self.set_running_config() + + def tearDown(self): + super(TestICXLinkaggModule, self).tearDown() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_exec_command.stop() + + def load_fixtures(self, commands=None): + compares = None + + def load_from_file(*args, **kwargs): + module = args + for arg in args: + if arg.params['check_running_config'] is True: + return load_fixture('lag_running_config.txt').strip() + else: + return '' + + self.get_config.side_effect = load_from_file + self.load_config.return_value = None + + def test_icx_linkage_create_new_LAG(self): + set_module_args(dict(group=10, name="LAG3", mode='static', members=['ethernet 1/1/4 to ethernet 1/1/7'])) + if not self.ENV_ICX_USE_DIFF: + commands = ['lag LAG3 static id 10', 'ports ethernet 1/1/4 to ethernet 1/1/7', 'exit'] + self.execute_module(commands=commands, changed=True) + else: + commands = ['lag LAG3 static id 10', 'ports ethernet 1/1/4 to ethernet 1/1/7', 'exit'] + self.execute_module(commands=commands, changed=True) + + def test_icx_linkage_modify_LAG(self): + set_module_args(dict(group=100, name="LAG1", mode='dynamic', members=['ethernet 1/1/4 to 1/1/7'])) + if not self.ENV_ICX_USE_DIFF: + commands = [ + 'lag LAG1 dynamic id 100', + 'ports ethernet 1/1/4 to 1/1/7', + 'exit' + ] + self.execute_module(commands=commands, changed=True) + else: + commands = [ + 'lag LAG1 dynamic id 100', + 'no ports ethernet 1/1/3', + 'no ports ethernet 1/1/8', + 'ports ethernet 1/1/4', + 'exit' + ] + self.execute_module(commands=commands, changed=True) + + def test_icx_linkage_modify_LAG_compare(self): + set_module_args(dict(group=100, name="LAG1", mode='dynamic', members=['ethernet 1/1/4 to 1/1/7'], check_running_config=True)) + if self.get_running_config(compare=True): + if not self.ENV_ICX_USE_DIFF: + commands = [ + 'lag LAG1 dynamic id 100', + 'no ports ethernet 1/1/3', + 'no ports ethernet 1/1/8', + 'ports ethernet 1/1/4', + 'exit' + ] + self.execute_module(commands=commands, changed=True) + else: + commands = [ + 'lag LAG1 dynamic id 100', + 'no ports ethernet 1/1/3', + 'no ports ethernet 1/1/8', + 'ports ethernet 1/1/4', + 'exit' + ] + self.execute_module(commands=commands, changed=True) + + def test_icx_linkage_purge_LAG(self): + set_module_args(dict(aggregate=[dict(group=100, name="LAG1", mode='dynamic')], purge=True)) + if not self.ENV_ICX_USE_DIFF: + commands = [ + 'lag LAG1 dynamic id 100', + 'exit' + ] + self.execute_module(commands=commands, changed=True) + else: + commands = [ + 'lag LAG1 dynamic id 100', + 'exit', + 'no lag LAG2 dynamic id 200' + ] + self.execute_module(commands=commands, changed=True) + + def test_icx_linkage_remove_LAG(self): + set_module_args(dict(group=100, name="LAG1", mode='dynamic', members=['ethernet 1/1/4 to 1/1/7'], state='absent')) + if not self.ENV_ICX_USE_DIFF: + commands = [ + 'no lag LAG1 dynamic id 100' + ] + self.execute_module(commands=commands, changed=True) + else: + commands = [ + 'no lag LAG1 dynamic id 100' + ] + self.execute_module(commands=commands, changed=True)