diff --git a/lib/ansible/modules/network/ios/ios_linkagg.py b/lib/ansible/modules/network/ios/ios_linkagg.py new file mode 100644 index 00000000000..96e15cd3182 --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_linkagg.py @@ -0,0 +1,311 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# 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': 'network'} + +DOCUMENTATION = """ +--- +module: ios_linkagg +version_added: "2.5" +author: "Trishna Guha (@trishnaguha)" +short_description: Manage link aggregation groups on Cisco IOS network devices +description: + - This module provides declarative management of link aggregation groups + on Cisco IOS network devices. +notes: + - Tested against IOS 15.2 +options: + group: + description: + - Channel-group number for the port-channel + Link aggregation group. Range 1-255. + mode: + description: + - Mode of the link aggregation group. + choices: ['active', 'on', 'passive', 'auto', 'desirable'] + members: + description: + - List of members of the link aggregation group. + aggregate: + description: List of link aggregation definitions. + state: + description: + - State of the link aggregation group. + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +- name: create link aggregation group + ios_linkagg: + group: 10 + state: present + +- name: delete link aggregation group + ios_linkagg: + group: 10 + state: absent + +- name: set link aggregation group to members + ios_linkagg: + group: 200 + mode: active + members: + - GigabitEthernet0/0 + - GigabitEthernet0/1 + +- name: remove link aggregation group from GigabitEthernet0/0 + ios_linkagg: + group: 200 + mode: active + members: + - GigabitEthernet0/1 + +- name: Create aggregate of linkagg definitions + ios_linkagg: + aggregate: + - { group: 3, mode: on, members: [GigabitEthernet0/1] } + - { group: 100, mode: passive, members: [GigabitEthernet0/2] } +""" + +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: + - interface port-channel 30 + - interface GigabitEthernet0/3 + - channel-group 30 mode on + - no interface port-channel 30 +""" + +import re +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.config import CustomNetworkConfig +from ansible.module_utils.network.common.utils import remove_default_spec +from ansible.module_utils.network.ios.ios import get_config, load_config +from ansible.module_utils.network.ios.ios import ios_argument_spec + + +def search_obj_in_list(group, lst): + for o in lst: + if o['group'] == group: + return o + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + purge = module.params['purge'] + + for w in want: + group = w['group'] + mode = w['mode'] + members = w.get('members') or [] + state = w['state'] + del w['state'] + + obj_in_have = search_obj_in_list(group, have) + + if state == 'absent': + if obj_in_have: + commands.append('no interface port-channel {0}'.format(group)) + + elif state == 'present': + cmd = ['interface port-channel {0}'.format(group), + 'end'] + if not obj_in_have: + if not group: + module.fail_json(msg='group is a required option') + commands.extend(cmd) + + if members: + for m in members: + commands.append('interface {0}'.format(m)) + commands.append('channel-group {0} mode {1}'.format(group, mode)) + + else: + if members: + if 'members' not in obj_in_have.keys(): + for m in members: + commands.extend(cmd) + commands.append('interface {0}'.format(m)) + commands.append('channel-group {0} mode {1}'.format(group, mode)) + + elif set(members) != set(obj_in_have['members']): + missing_members = list(set(members) - set(obj_in_have['members'])) + for m in missing_members: + commands.extend(cmd) + commands.append('interface {0}'.format(m)) + commands.append('channel-group {0} mode {1}'.format(group, mode)) + + superfluous_members = list(set(obj_in_have['members']) - set(members)) + for m in superfluous_members: + commands.extend(cmd) + commands.append('interface {0}'.format(m)) + commands.append('no channel-group {0} mode {1}'.format(group, mode)) + + if purge: + for h in have: + obj_in_want = search_obj_in_list(h['group'], want) + if not obj_in_want: + commands.append('no interface port-channel {0}'.format(h['group'])) + + return commands + + +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'] + }) + + return obj + + +def parse_mode(module, config, group, member): + mode = None + netcfg = CustomNetworkConfig(indent=1, contents=config) + parents = ['interface {0}'.format(member)] + body = netcfg.get_section(parents) + + match_int = re.findall(r'interface {0}\n'.format(member), body, re.M) + if match_int: + match = re.search(r'channel-group {0} mode (\S+)'.format(group), body, re.M) + if match: + mode = match.group(1) + + return mode + + +def parse_members(module, config, group): + members = [] + + for line in config.strip().split('!'): + l = line.strip() + if l.startswith('interface'): + match_group = re.findall(r'channel-group {0} mode'.format(group), l, re.M) + if match_group: + match = re.search(r'interface (\S+)', l, re.M) + if match: + members.append(match.group(1)) + + return members + + +def get_channel(module, config, group): + match = re.findall(r'interface (\S+)', config, re.M) + + if not match: + return {} + + channel = {} + for item in set(match): + member = item + channel['mode'] = parse_mode(module, config, group, member) + channel['members'] = parse_members(module, config, group) + + return channel + + +def map_config_to_obj(module): + objs = list() + config = get_config(module) + + for line in config.split('\n'): + l = line.strip() + match = re.search(r'interface Port-channel(\S+)', l, re.M) + if match: + obj = {} + group = match.group(1) + obj['group'] = group + obj.update(get_channel(module, config, group)) + objs.append(obj) + + return objs + + +def main(): + """ main entry point for module execution + """ + element_spec = dict( + group=dict(type='int'), + mode=dict(choices=['active', 'on', 'passive', 'auto', 'desirable']), + members=dict(type='list'), + state=dict(default='present', + choices=['present', 'absent']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['group'] = dict(required=True) + + required_one_of = [['group', 'aggregate']] + required_together = [['members', 'mode']] + mutually_exclusive = [['group', 'aggregate']] + + # remove default in aggregate spec, to handle common arguments + 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) + argument_spec.update(ios_argument_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} + 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/integration/ios.yaml b/test/integration/ios.yaml index 5094aeb2009..8a3521a688a 100644 --- a/test/integration/ios.yaml +++ b/test/integration/ios.yaml @@ -121,14 +121,24 @@ failed_modules: "{{ failed_modules }} + [ 'ios_lldp' ]" test_failed: true -# - block: -# - include_role: -# name: ios_vlan -# when: "limit_to in ['*', 'ios_vlan']" -# rescue: -# - set_fact: -# failed_modules: "{{ failed_modules }} + [ 'ios_vlan' ]" -# test_failed: true + - block: + - include_role: + name: ios_vlan + when: "limit_to in ['*', 'ios_vlan']" + rescue: + - set_fact: + failed_modules: "{{ failed_modules }} + [ 'ios_vlan' ]" + test_failed: true + + - block: + - include_role: + name: ios_linkagg + when: "limit_to in ['*', 'ios_linkagg']" + rescue: + - set_fact: + failed_modules: "{{ failed_modules }} + [ 'ios_linkagg' ]" + test_failed: true + ########### - debug: var=failed_modules diff --git a/test/integration/targets/ios_linkagg/defaults/main.yaml b/test/integration/targets/ios_linkagg/defaults/main.yaml new file mode 100644 index 00000000000..9ef5ba51651 --- /dev/null +++ b/test/integration/targets/ios_linkagg/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "*" +test_items: [] diff --git a/test/integration/targets/ios_linkagg/meta/main.yaml b/test/integration/targets/ios_linkagg/meta/main.yaml new file mode 100644 index 00000000000..159cea8d383 --- /dev/null +++ b/test/integration/targets/ios_linkagg/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - prepare_ios_tests diff --git a/test/integration/targets/ios_linkagg/tasks/cli.yaml b/test/integration/targets/ios_linkagg/tasks/cli.yaml new file mode 100644 index 00000000000..303af407622 --- /dev/null +++ b/test/integration/targets/ios_linkagg/tasks/cli.yaml @@ -0,0 +1,22 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + +- name: run test case (connection=local) + include: "{{ test_case_to_run }} ansible_connection=local" + with_first_found: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ios_linkagg/tasks/main.yaml b/test/integration/targets/ios_linkagg/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/ios_linkagg/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/ios_linkagg/tests/cli/basic.yaml b/test/integration/targets/ios_linkagg/tests/cli/basic.yaml new file mode 100644 index 00000000000..77995d4965a --- /dev/null +++ b/test/integration/targets/ios_linkagg/tests/cli/basic.yaml @@ -0,0 +1,171 @@ +--- +- debug: msg="START cli/basic.yaml on connection={{ ansible_connection }}" + +- set_fact: switch_type="{{ switch_type }}" + +- block: + + - name: setup - remove config used in test(part1) + ios_config: + lines: + - no interface port-channel 20 + authorize: yes + ignore_errors: yes + + - name: setup - remove config used in test(part2) + ios_config: + lines: + - no interface port-channel 5 + authorize: yes + ignore_errors: yes + + - name: setup - remove config used in test(part3) + ios_config: + lines: + - no channel-group 20 mode active + authorize: yes + parents: "{{ item }}" + loop: + - interface GigabitEthernet0/1 + - interface GigabitEthernet0/2 + + - name: create linkagg + ios_linkagg: &create + group: 20 + state: present + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'interface port-channel 20' in result.commands" + + - name: create linkagg(Idempotence) + ios_linkagg: *create + register: result + + - assert: + that: + - "result.changed == false" + + - name: set link aggregation group to members + ios_linkagg: &configure_member + group: 20 + mode: active + members: + - GigabitEthernet0/1 + - GigabitEthernet0/2 + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'interface GigabitEthernet0/1' in result.commands" + - "'channel-group 20 mode active' in result.commands" + - "'interface GigabitEthernet0/2' in result.commands" + - "'channel-group 20 mode active' in result.commands" + + - name: set link aggregation group to members(Idempotence) + ios_linkagg: *configure_member + register: result + + - assert: + that: + - "result.changed == false" + + - name: remove link aggregation group from member + ios_linkagg: &remove_member + group: 20 + mode: active + members: + - GigabitEthernet0/2 + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'interface GigabitEthernet0/1' in result.commands" + - "'no channel-group 20 mode active' in result.commands" + + - name: remove link aggregation group from member(Idempotence) + ios_linkagg: *remove_member + register: result + + - assert: + that: + - "result.changed == false" + + - name: remove linkagg + ios_linkagg: &remove + group: 20 + state: absent + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'no interface port-channel 20' in result.commands" + + - name: remove linkagg(Idempotence) + ios_linkagg: *remove + register: result + + - assert: + that: + - "result.changed == false" + + - name: create aggregate of linkagg definitions + ios_linkagg: &create_agg + aggregate: + - { group: 5 } + - { group: 20, mode: active, members: ['GigabitEthernet0/1'] } + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'interface port-channel 5' in result.commands" + - "'interface port-channel 20' in result.commands" + - "'interface GigabitEthernet0/1' in result.commands" + - "'channel-group 20 mode active' in result.commands" + + - name: create aggregate of linkagg definitions(Idempotence) + ios_linkagg: *create_agg + register: result + + - assert: + that: + - "result.changed == false" + + - name: teardown(part1) + ios_config: + lines: + - no interface port-channel 20 + authorize: yes + ignore_errors: yes + + - name: teardown(part2) + ios_config: + lines: + - no interface port-channel 5 + authorize: yes + ignore_errors: yes + + - name: teardown(part3) + ios_config: + lines: + - no channel-group 20 mode active + authorize: yes + parents: "{{ item }}" + loop: + - interface GigabitEthernet0/1 + - interface GigabitEthernet0/2 + + when: switch_type == 'L2' + +- debug: msg="END cli/basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ios_vlan/tests/cli/basic.yaml b/test/integration/targets/ios_vlan/tests/cli/basic.yaml index 2f6983355f0..7d6a9ee7f84 100644 --- a/test/integration/targets/ios_vlan/tests/cli/basic.yaml +++ b/test/integration/targets/ios_vlan/tests/cli/basic.yaml @@ -1,203 +1,213 @@ -#--- -#- name: setup - remove vlan used in test -# ios_config: -# lines: -# - no vlan 100 -# - no vlan 200 -# - no vlan 300 -# authorize: yes - -#- name: setup - remove switchport settings on interfaces used in test -# ios_config: -# lines: -# - switchport mode access -# - no switchport access vlan 100 -# authorize: yes -# parents: "{{ item }}" -# with_items: -# - interface GigabitEthernet0/1 -# - interface GigabitEthernet0/2 - -#- name: create vlan -# ios_vlan: &create -# vlan_id: 100 -# name: test-vlan -# authorize: yes -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'vlan 100' in result.commands" -# - "'name test-vlan' in result.commands" - -#- name: create vlan(idempotence) -# ios_vlan: *create -# register: result - -#- assert: -# that: -# - "result.changed == false" - -#- name: Add interfaces to vlan -# ios_vlan: &interfaces -# vlan_id: 100 -# interfaces: -# - GigabitEthernet0/1 -# - GigabitEthernet0/2 -# authorize: yes -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'interface GigabitEthernet0/1' in result.commands" -# - "'switchport mode access' in result.commands" -# - "'switchport access vlan 100' in result.commands" -# - "'interface GigabitEthernet0/2' in result.commands" -# - "'switchport mode access' in result.commands" -# - "'switchport access vlan 100' in result.commands" - -#- name: Add interfaces to vlan(idempotence) -# ios_vlan: *interfaces -# register: result - -#- assert: -# that: -# - "result.changed == false" - -#- name: Remove interface from vlan -# ios_vlan: &single_int -# vlan_id: 100 -# interfaces: -# - GigabitEthernet0/1 -# authorize: yes -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'vlan 100' in result.commands" -# - "'interface GigabitEthernet0/2' in result.commands" -# - "'switchport mode access' in result.commands" -# - "'no switchport access vlan 100' in result.commands" - -#- name: Remove interface from vlan(idempotence) -# ios_vlan: *single_int -# register: result - -#- assert: -# that: -# - "result.changed == false" - -#- name: Suspend vlan -# ios_vlan: -# vlan_id: 100 -# state: suspend -# authorize: yes -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'vlan 100' in result.commands" -# - "'state suspend' in result.commands" - -#- name: Unsuspend vlan -# ios_vlan: -# vlan_id: 100 -# state: active -# authorize: yes -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'vlan 100' in result.commands" -# - "'state active' in result.commands" - -#- name: delete vlan -# ios_vlan: &delete -# vlan_id: 100 -# authorize: yes -# state: absent -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'no vlan 100' in result.commands" - -#- name: delete vlan(idempotence) -# ios_vlan: *delete -# register: result - -#- assert: -# that: -# - "result.changed == false" - -#- name: create vlans using aggregate -# ios_vlan: &create_aggregate -# aggregate: -# - { vlan_id: 200, name: vlan-200 } -# - { vlan_id: 300, name: vlan-300 } -# authorize: yes -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'vlan 200' in result.commands" -# - "'name vlan-200' in result.commands" -# - "'vlan 300' in result.commands" -# - "'name vlan-300' in result.commands" - -#- name: create vlans using aggregate(idempotence) -# ios_vlan: *create_aggregate -# register: result - -#- assert: -# that: -# - "result.changed == false" - -#- name: delete vlans using aggregate -# ios_vlan: &delete_aggregate -# aggregate: -# - { vlan_id: 200, name: vlan-200 } -# - { vlan_id: 300, name: vlan-300 } -# state: absent -# authorize: yes -# register: result - -#- assert: -# that: -# - "result.changed == true" -# - "'no vlan 200' in result.commands" -# - "'no vlan 300' in result.commands" - -#- name: delete vlans using aggregate(idempotence) -# ios_vlan: *delete_aggregate -# register: result - -#- assert: -# that: -# - "result.changed == false" - -#- name: teardown(part1) -# ios_config: -# lines: -# - no vlan 100 -# - no vlan 200 -# - no vlan 300 -# authorize: yes - -#- name: teardown(part2) -# ios_config: -# lines: -# - switchport mode access -# - no switchport access vlan 100 -# authorize: yes -# parents: "{{ item }}" -# with_items: -# - interface GigabitEthernet0/1 -# - interface GigabitEthernet0/2 +--- +- debug: msg="START cli/basic.yaml on connection={{ ansible_connection }}" + +- set_fact: switch_type="{{ switch_type }}" + +- block: + + - name: setup - remove vlan used in test + ios_config: + lines: + - no vlan 100 + - no vlan 200 + - no vlan 300 + authorize: yes + + - name: setup - remove switchport settings on interfaces used in test + ios_config: + lines: + - switchport mode access + - no switchport access vlan 100 + authorize: yes + parents: "{{ item }}" + loop: + - interface GigabitEthernet0/1 + - interface GigabitEthernet0/2 + + - name: create vlan + ios_vlan: &create + vlan_id: 100 + name: test-vlan + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'vlan 100' in result.commands" + - "'name test-vlan' in result.commands" + + - name: create vlan(idempotence) + ios_vlan: *create + register: result + + - assert: + that: + - "result.changed == false" + + - name: Add interfaces to vlan + ios_vlan: &interfaces + vlan_id: 100 + interfaces: + - GigabitEthernet0/1 + - GigabitEthernet0/2 + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'interface GigabitEthernet0/1' in result.commands" + - "'switchport mode access' in result.commands" + - "'switchport access vlan 100' in result.commands" + - "'interface GigabitEthernet0/2' in result.commands" + - "'switchport mode access' in result.commands" + - "'switchport access vlan 100' in result.commands" + + - name: Add interfaces to vlan(idempotence) + ios_vlan: *interfaces + register: result + + - assert: + that: + - "result.changed == false" + + - name: Remove interface from vlan + ios_vlan: &single_int + vlan_id: 100 + interfaces: + - GigabitEthernet0/1 + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'vlan 100' in result.commands" + - "'interface GigabitEthernet0/2' in result.commands" + - "'switchport mode access' in result.commands" + - "'no switchport access vlan 100' in result.commands" + + - name: Remove interface from vlan(idempotence) + ios_vlan: *single_int + register: result + + - assert: + that: + - "result.changed == false" + + - name: Suspend vlan + ios_vlan: + vlan_id: 100 + state: suspend + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'vlan 100' in result.commands" + - "'state suspend' in result.commands" + + - name: Unsuspend vlan + ios_vlan: + vlan_id: 100 + state: active + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'vlan 100' in result.commands" + - "'state active' in result.commands" + + - name: delete vlan + ios_vlan: &delete + vlan_id: 100 + authorize: yes + state: absent + register: result + + - assert: + that: + - "result.changed == true" + - "'no vlan 100' in result.commands" + + - name: delete vlan(idempotence) + ios_vlan: *delete + register: result + + - assert: + that: + - "result.changed == false" + + - name: create vlans using aggregate + ios_vlan: &create_aggregate + aggregate: + - { vlan_id: 200, name: vlan-200 } + - { vlan_id: 300, name: vlan-300 } + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'vlan 200' in result.commands" + - "'name vlan-200' in result.commands" + - "'vlan 300' in result.commands" + - "'name vlan-300' in result.commands" + + - name: create vlans using aggregate(idempotence) + ios_vlan: *create_aggregate + register: result + + - assert: + that: + - "result.changed == false" + + - name: delete vlans using aggregate + ios_vlan: &delete_aggregate + aggregate: + - { vlan_id: 200, name: vlan-200 } + - { vlan_id: 300, name: vlan-300 } + state: absent + authorize: yes + register: result + + - assert: + that: + - "result.changed == true" + - "'no vlan 200' in result.commands" + - "'no vlan 300' in result.commands" + + - name: delete vlans using aggregate(idempotence) + ios_vlan: *delete_aggregate + register: result + + - assert: + that: + - "result.changed == false" + + - name: teardown(part1) + ios_config: + lines: + - no vlan 100 + - no vlan 200 + - no vlan 300 + authorize: yes + + - name: teardown(part2) + ios_config: + lines: + - switchport mode access + - no switchport access vlan 100 + authorize: yes + parents: "{{ item }}" + loop: + - interface GigabitEthernet0/1 + - interface GigabitEthernet0/2 + + when: switch_type == 'L2' + +- debug: msg="END cli/basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/prepare_ios_tests/tasks/main.yml b/test/integration/targets/prepare_ios_tests/tasks/main.yml index d4b89ae4c9f..6a41e038f53 100644 --- a/test/integration/targets/prepare_ios_tests/tasks/main.yml +++ b/test/integration/targets/prepare_ios_tests/tasks/main.yml @@ -11,3 +11,15 @@ # http://jinja.pocoo.org/docs/2.9/templates/#truncate - set_fact: shorter_hostname: '{{ inventory_hostname_short| truncate(10, True, "") }}' + +- name: "Discover IOS L2/L3 switch type" + ios_command: + commands: ['show version'] + connection: network_cli + become: yes + register: result + +- set_fact: switch_type="" + +- set_fact: switch_type="L2" + when: '"l2" in result.stdout[0]'