Add iosxr_lag_interfaces resource module (#61303)

Signed-off-by: NilashishC <nilashishchakraborty8@gmail.com>
This commit is contained in:
Nilashish Chakraborty 2019-08-27 23:01:05 +05:30 committed by GitHub
parent a213b9160c
commit 36f4af203c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1800 additions and 17 deletions

View file

@ -32,6 +32,8 @@ class FactsArgs(object): # pylint: disable=R0903
'!interfaces', '!interfaces',
'l2_interfaces', 'l2_interfaces',
'!l2_interfaces', '!l2_interfaces',
'lag_interfaces',
'!lag_interfaces'
] ]
argument_spec = { argument_spec = {

View file

@ -0,0 +1,87 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The arg spec for the iosxr_lag_interfaces module
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class Lag_interfacesArgs(object): # pylint: disable=R0903
"""The arg spec for the iosxr_lag_interfaces module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'config': {
'elements': 'dict',
'options': {
'links': {
'options': {
'max_active': {
'type': 'int'
},
'min_active': {
'type': 'int'
}
},
'type': 'dict'
},
'load_balancing_hash': {
'choices': ['dst-ip', 'src-ip'],
'type': 'str'
},
'members': {
'elements': 'dict',
'options': {
'member': {
'type': 'str'
},
'mode': {
'choices': ['on', 'active', 'passive', 'inherit'],
'type': 'str'
}
},
'type': 'list'
},
'mode': {
'choices': ['on', 'active', 'passive'],
'type': 'str'
},
'name': {
'required': True,
'type': 'str'
}
},
'type': 'list'
},
'state': {
'choices': ['merged', 'replaced', 'overridden', 'deleted'],
'default': 'merged',
'type': 'str'
}
}

View file

@ -0,0 +1,383 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The iosxr_lag_interfaces class
It is in this file where the current configuration (as dict)
is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from copy import deepcopy
from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common.cfg.base import ConfigBase
from ansible.module_utils.network.iosxr.facts.facts import Facts
from ansible.module_utils.network.common.utils \
import (
to_list,
dict_diff,
remove_empties,
search_obj_in_list,
param_list_to_dict
)
from ansible.module_utils.network.iosxr.utils.utils \
import (
diff_list_of_dicts,
pad_commands,
flatten_dict,
dict_delete,
normalize_interface
)
class Lag_interfaces(ConfigBase):
"""
The iosxr_lag_interfaces class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'lag_interfaces',
]
def __init__(self, module):
super(Lag_interfaces, self).__init__(module)
def get_lag_interfaces_facts(self):
""" Get the 'facts' (the current configuration)
:rtype: A dictionary
:returns: The current configuration as a dictionary
"""
facts, _warnings = Facts(self._module).get_facts(
self.gather_subset, self.gather_network_resources)
lag_interfaces_facts = facts['ansible_network_resources'].get(
'lag_interfaces')
if not lag_interfaces_facts:
return []
return lag_interfaces_facts
def execute_module(self):
""" Execute the module
:rtype: A dictionary
:returns: The result from module execution
"""
result = {'changed': False}
warnings = list()
commands = list()
existing_lag_interfaces_facts = self.get_lag_interfaces_facts()
commands.extend(self.set_config(existing_lag_interfaces_facts))
if commands:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['commands'] = commands
changed_lag_interfaces_facts = self.get_lag_interfaces_facts()
result['before'] = existing_lag_interfaces_facts
if result['changed']:
result['after'] = changed_lag_interfaces_facts
result['warnings'] = warnings
return result
def set_config(self, existing_lag_interfaces_facts):
""" Collect the configuration from the args passed to the module,
collect the current configuration (as a dict from facts)
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
want = self._module.params['config']
if want:
for item in want:
item['name'] = normalize_interface(item['name'])
if 'members' in want and want['members']:
for item in want['members']:
item.update({
'member': normalize_interface(item['member']),
'mode': item['mode']
})
have = existing_lag_interfaces_facts
resp = self.set_state(want, have)
return to_list(resp)
def set_state(self, want, have):
""" Select the appropriate function based on the state provided
:param want: the desired configuration as a dictionary
:param have: the current configuration as a dictionary
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
state = self._module.params['state']
commands = []
if state == 'overridden':
commands.extend(self._state_overridden(want, have))
elif state == 'deleted':
commands.extend(self._state_deleted(want, have))
else:
# Instead of passing entire want and have
# list of dictionaries to the respective
# _state_* methods we are passing the want
# and have dictionaries per interface
for item in want:
name = item['name']
obj_in_have = search_obj_in_list(name, have)
if state == 'merged':
commands.extend(self._state_merged(item, obj_in_have))
elif state == 'replaced':
commands.extend(self._state_replaced(item, obj_in_have))
return commands
def _state_replaced(self, want, have):
""" The command generator when state is replaced
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
if have:
commands.extend(self._render_bundle_del_commands(want, have))
commands.extend(self._render_bundle_updates(want, have))
if commands or have == {}:
pad_commands(commands, want['name'])
if have:
commands.extend(self._render_interface_del_commands(want, have))
commands.extend(self._render_interface_updates(want, have))
return commands
def _state_overridden(self, want, have):
""" The command generator when state is overridden
:rtype: A list
:returns: the commands necessary to migrate the current configuration
to the desired configuration
"""
commands = []
for have_intf in have:
intf_in_want = search_obj_in_list(have_intf['name'], want)
if not intf_in_want:
commands.extend(self._purge_attribs(have_intf))
for intf in want:
intf_in_have = search_obj_in_list(intf['name'], have)
commands.extend(self._state_replaced(intf, intf_in_have))
return commands
def _state_merged(self, want, have):
""" The command generator when state is merged
:rtype: A list
:returns: the commands necessary to merge the provided into
the current configuration
"""
commands = []
commands.extend(self._render_bundle_updates(want, have))
if commands or have == {}:
pad_commands(commands, want['name'])
commands.extend(self._render_interface_updates(want, have))
return commands
def _state_deleted(self, want, have):
""" The command generator when state is deleted
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
"""
commands = []
if not want:
for item in have:
commands.extend(self._purge_attribs(intf=item))
else:
for item in want:
name = item['name']
obj_in_have = search_obj_in_list(name, have)
if not obj_in_have:
self._module.fail_json(
msg=('interface {0} does not exist'.format(name)))
commands.extend(self._purge_attribs(intf=obj_in_have))
return commands
def _render_bundle_updates(self, want, have):
""" The command generator for updates to bundles
:rtype: A list
:returns: the commands necessary to update bundles
"""
commands = []
if not have:
have = {'name': want['name']}
want_copy = deepcopy(want)
have_copy = deepcopy(have)
want_copy.pop('members', [])
have_copy.pop('members', [])
bundle_updates = dict_diff(have_copy, want_copy)
if bundle_updates:
for key, value in iteritems(
flatten_dict(remove_empties(bundle_updates))):
commands.append(self._compute_commands(key=key, value=value))
return commands
def _render_interface_updates(self, want, have):
""" The command generator for updates to member
interfaces
:rtype: A list
:returns: the commands necessary to update member
interfaces
"""
commands = []
if not have:
have = {'name': want['name']}
member_diff = diff_list_of_dicts(want['members'],
have.get('members', []))
for diff in member_diff:
diff_cmd = []
bundle_cmd = 'bundle id {0}'.format(
want['name'].split('Bundle-Ether')[1])
if diff.get('mode'):
bundle_cmd += ' mode {0}'.format(diff.get('mode'))
diff_cmd.append(bundle_cmd)
pad_commands(diff_cmd, diff['member'])
commands.extend(diff_cmd)
return commands
def _render_bundle_del_commands(self, want, have):
""" The command generator for delete commands
w.r.t bundles
:rtype: A list
:returns: the commands necessary to update member
interfaces
"""
commands = []
if not want:
want = {'name': have['name']}
want_copy = deepcopy(want)
have_copy = deepcopy(have)
want_copy.pop('members', [])
have_copy.pop('members', [])
to_delete = dict_delete(have_copy, remove_empties(want_copy))
if to_delete:
for key, value in iteritems(flatten_dict(
remove_empties(to_delete))):
commands.append(
self._compute_commands(key=key, value=value, remove=True))
return commands
def _render_interface_del_commands(self, want, have):
""" The command generator for delete commands
w.r.t member interfaces
:rtype: A list
:returns: the commands necessary to update member
interfaces
"""
commands = []
if not want:
want = {}
have_members = have.get('members')
if have_members:
have_members = param_list_to_dict(deepcopy(have_members), unique_key='member')
want_members = param_list_to_dict(deepcopy(want).get('members', []), unique_key='member')
for key in have_members:
if key not in want_members:
member_cmd = ['no bundle id']
pad_commands(member_cmd, key)
commands.extend(member_cmd)
return commands
def _purge_attribs(self, intf):
""" The command generator for purging attributes
:rtype: A list
:returns: the commands necessary to purge attributes
"""
commands = []
have_copy = deepcopy(intf)
members = have_copy.pop('members', [])
to_delete = dict_delete(have_copy, remove_empties({'name': have_copy['name']}))
if to_delete:
for key, value in iteritems(flatten_dict(remove_empties(to_delete))):
commands.append(self._compute_commands(key=key, value=value, remove=True))
if commands:
pad_commands(commands, intf['name'])
if members:
members = param_list_to_dict(deepcopy(members), unique_key='member')
for key in members:
member_cmd = ['no bundle id']
pad_commands(member_cmd, key)
commands.extend(member_cmd)
return commands
def _compute_commands(self, key, value, remove=False):
""" The method generates LAG commands based on the
key, value passed. When remove is set to True,
the command is negated.
:rtype: str
:returns: a command based on the `key`, `value` pair
passed and the value of `remove`
"""
if key == "mode":
cmd = "lacp mode {0}".format(value)
elif key == "load_balancing_hash":
cmd = "bundle load-balancing hash {0}".format(value)
elif key == "max_active":
cmd = "bundle maximum-active links {0}".format(value)
elif key == "min_active":
cmd = "bundle minimum-active links {0}".format(value)
if remove:
cmd = "no {0}".format(cmd)
return cmd

View file

@ -20,6 +20,7 @@ from ansible.module_utils.network.iosxr.facts.lacp_interfaces.lacp_interfaces im
from ansible.module_utils.network.iosxr.facts.lldp_global.lldp_global import Lldp_globalFacts from ansible.module_utils.network.iosxr.facts.lldp_global.lldp_global import Lldp_globalFacts
from ansible.module_utils.network.iosxr.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts from ansible.module_utils.network.iosxr.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts
from ansible.module_utils.network.iosxr.facts.interfaces.interfaces import InterfacesFacts from ansible.module_utils.network.iosxr.facts.interfaces.interfaces import InterfacesFacts
from ansible.module_utils.network.iosxr.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts
from ansible.module_utils.network.iosxr.facts.legacy.base import Default, Hardware, Interfaces, Config from ansible.module_utils.network.iosxr.facts.legacy.base import Default, Hardware, Interfaces, Config
from ansible.module_utils.network.iosxr.facts.l2_interfaces.l2_interfaces import L2_InterfacesFacts from ansible.module_utils.network.iosxr.facts.l2_interfaces.l2_interfaces import L2_InterfacesFacts
from ansible.module_utils.network.iosxr.facts.legacy.\ from ansible.module_utils.network.iosxr.facts.legacy.\
@ -39,6 +40,7 @@ FACT_RESOURCE_SUBSETS = dict(
lldp_interfaces=Lldp_interfacesFacts, lldp_interfaces=Lldp_interfacesFacts,
interfaces=InterfacesFacts, interfaces=InterfacesFacts,
l2_interfaces=L2_InterfacesFacts, l2_interfaces=L2_InterfacesFacts,
lag_interfaces=Lag_interfacesFacts
) )

View file

@ -0,0 +1,128 @@
#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""
The iosxr lag_interfaces fact class
It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import re
from copy import deepcopy
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.iosxr.argspec.lag_interfaces.lag_interfaces import Lag_interfacesArgs
class Lag_interfacesFacts(object):
""" The iosxr lag_interfaces fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = Lag_interfacesArgs.argument_spec
spec = deepcopy(self.argument_spec)
if subspec:
if options:
facts_argument_spec = spec[subspec][options]
else:
facts_argument_spec = spec[subspec]
else:
facts_argument_spec = spec
self.generated_spec = utils.generate_dict(facts_argument_spec)
def populate_facts(self, connection, ansible_facts, data=None):
""" Populate the facts for lag_interfaces
:param connection: the device connection
:param ansible_facts: Facts dictionary
:param data: previously collected conf
:rtype: dictionary
:returns: facts
"""
if not data:
data = connection.get_config(flags='interface')
interfaces = data.split('interface ')
objs = []
for interface in interfaces:
if interface.startswith("Bundle-Ether"):
obj = self.render_config(self.generated_spec, interface, interfaces)
if obj:
objs.append(obj)
ansible_facts['ansible_network_resources'].pop('lag_interfaces', None)
facts = {}
facts['lag_interfaces'] = []
params = utils.validate_config(self.argument_spec, {'config': objs})
for cfg in params['config']:
facts['lag_interfaces'].append(utils.remove_empties(cfg))
ansible_facts['ansible_network_resources'].update(facts)
return ansible_facts
def render_config(self, spec, conf, data):
"""
Render config as dictionary structure and delete keys
from spec for null values
:param spec: The facts tree, generated from the argspec
:param conf: The configuration
:rtype: dictionary
:returns: The generated config
"""
config = deepcopy(spec)
match = re.search(r'(Bundle-Ether)(\d+)', conf, re.M)
if match:
config['name'] = match.group(1) + match.group(2)
config['load_balancing_hash'] = utils.parse_conf_arg(
conf, 'bundle load-balancing hash')
config['mode'] = utils.parse_conf_arg(conf, 'lacp mode')
config['links']['max_active'] = utils.parse_conf_arg(
conf, 'bundle maximum-active links')
config['links']['min_active'] = utils.parse_conf_arg(
conf, 'bundle minimum-active links')
config['members'] = self.parse_members(match.group(2), data)
return utils.remove_empties(config)
def parse_members(self, bundle_id, interfaces):
"""
Renders a list of member interfaces for every bundle
present in running-config.
:param bundle_id: The Bundle-Ether ID fetched from running-config
:param interfaces: Data of all interfaces present in running-config
:rtype: list
:returns: A list of member interfaces
"""
def _parse_interface(name):
if name.startswith('preconfigure'):
return name.split()[1]
else:
return name.split()[0]
members = []
for interface in interfaces:
if not interface.startswith('Bu'):
match = re.search(r'bundle id (\d+) mode (\S+)', interface, re.M)
if match:
if bundle_id == match.group(1):
members.append(
{
'member': _parse_interface(interface),
'mode': match.group(2)
}
)
return members

View file

@ -126,6 +126,27 @@ def pad_commands(commands, interface):
commands.insert(0, 'interface {0}'.format(interface)) commands.insert(0, 'interface {0}'.format(interface))
def diff_list_of_dicts(w, h):
"""
Returns a list containing diff between
two list of dictionaries
"""
if not w:
w = []
if not h:
h = []
diff = []
set_w = set(tuple(d.items()) for d in w)
set_h = set(tuple(d.items()) for d in h)
difference = set_w.difference(set_h)
for element in difference:
diff.append(dict((x, y) for x, y in element))
return diff
def normalize_interface(name): def normalize_interface(name):
"""Return the normalized interface name """Return the normalized interface name
""" """
@ -145,20 +166,20 @@ def normalize_interface(name):
if_type = 'FastEthernet' if_type = 'FastEthernet'
elif name.lower().startswith('fo'): elif name.lower().startswith('fo'):
if_type = 'FortyGigE' if_type = 'FortyGigE'
elif name.lower().startswith('et'): elif name.lower().startswith('te'):
if_type = 'Ethernet' if_type = 'TenGigE'
elif name.lower().startswith('vl'):
if_type = 'Vlan'
elif name.lower().startswith('lo'):
if_type = 'loopback'
elif name.lower().startswith('po'):
if_type = 'port-channel'
elif name.lower().startswith('nv'):
if_type = 'nve'
elif name.lower().startswith('twe'): elif name.lower().startswith('twe'):
if_type = 'TwentyFiveGigE' if_type = 'TwentyFiveGigE'
elif name.lower().startswith('hu'): elif name.lower().startswith('hu'):
if_type = 'HundredGigE' if_type = 'HundredGigE'
elif name.lower().startswith('vl'):
if_type = 'Vlan'
elif name.lower().startswith('lo'):
if_type = 'Loopback'
elif name.lower().startswith('be'):
if_type = 'Bundle-Ether'
elif name.lower().startswith('bp'):
if_type = 'Bundle-POS'
else: else:
if_type = None if_type = None
@ -188,12 +209,10 @@ def get_interface_type(interface):
return 'FortyGigE' return 'FortyGigE'
elif interface.upper().startswith('ET'): elif interface.upper().startswith('ET'):
return 'Ethernet' return 'Ethernet'
elif interface.upper().startswith('VL'):
return 'Vlan'
elif interface.upper().startswith('LO'): elif interface.upper().startswith('LO'):
return 'loopback' return 'Loopback'
elif interface.upper().startswith('PO'): elif interface.upper().startswith('BE'):
return 'port-channel' return 'Bundle-Ether'
elif interface.upper().startswith('NV'): elif interface.upper().startswith('NV'):
return 'nve' return 'nve'
elif interface.upper().startswith('TWE'): elif interface.upper().startswith('TWE'):

View file

@ -55,7 +55,8 @@ options:
specific subset should not be collected. specific subset should not be collected.
required: false required: false
choices: ['all', 'lacp', '!lacp', 'lacp_interfaces', '!lacp_interfaces', 'lldp_global', '!lldp_global', choices: ['all', 'lacp', '!lacp', 'lacp_interfaces', '!lacp_interfaces', 'lldp_global', '!lldp_global',
'lldp_interfaces', '!lldp_interfaces', 'interfaces', '!interfaces', 'l2_interfaces', '!l2_interfaces'] 'lldp_interfaces', '!lldp_interfaces', 'interfaces', '!interfaces', 'l2_interfaces', '!l2_interfaces',
'lag_interfaces', '!lag_interfaces']
version_added: "2.9" version_added: "2.9"
""" """

View file

@ -0,0 +1,639 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#############################################
# WARNING #
#############################################
#
# This file is auto generated by the resource
# module builder playbook.
#
# Do not edit this file manually.
#
# Changes to this file will be over written
# by the resource module builder.
#
# Changes should be made in the model used to
# generate this file or in the resource module
# builder template.
#
#############################################
"""
The module file for iosxr_lag_interfaces
"""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'network'
}
DOCUMENTATION = """
---
module: iosxr_lag_interfaces
version_added: 2.9
short_description: Manages attributes of LAG/Ether-Bundle interfaces on IOS-XR devices.
description:
- This module manages the attributes of LAG/Ether-Bundle interfaces on IOS-XR devices.
notes:
- Tested against IOS-XR 6.1.3.
- This module works with connection C(network_cli). See L(the IOS-XR Platform Options,../network/user_guide/platform_iosxr.html).
author: Nilashish Chakraborty (@NilashishC)
options:
config:
description: A provided Link Aggregation Group (LAG) configuration.
type: list
elements: dict
suboptions:
name:
description:
- Name/Identifier of the LAG/Ether-Bundle to configure.
type: str
required: True
members:
description:
- List of member interfaces for the LAG/Ether-Bundle.
type: list
elements: dict
suboptions:
member:
description:
- Name of the member interface.
type: str
mode:
description:
- Specifies the mode of the operation for the member interface.
- Mode 'active' runs LACP in active mode.
- Mode 'on' does not run LACP over the port.
- Mode 'passive' runs LACP in passive mode over the port.
- Mode 'inherit' runs LACP as configured in the bundle.
choices: ['on', 'active', 'passive', 'inherit']
type: str
mode:
description:
- LAG mode.
- Mode 'active' runs LACP in active mode over the port.
- Mode 'on' does not run LACP over the port.
- Mode 'passive' runs LACP in passive mode over the port.
choices: ['on', 'active', 'passive']
type: str
links:
description:
- This dict contains configurable options related to LAG/Ether-Bundle links.
type: dict
suboptions:
max_active:
description:
- Specifies the limit on the number of links that can be active in the LAG/Ether-Bundle.
- Refer to vendor documentation for valid values.
type: int
min_active:
description:
- Specifies the minimum number of active links needed to bring up the LAG/Ether-Bundle.
- Refer to vendor documentation for valid values.
type: int
load_balancing_hash:
description:
- Specifies the hash function used for traffic forwarded over the LAG/Ether-Bundle.
- Option 'dst-ip' uses the destination IP as the hash function.
- Option 'src-ip' uses the source IP as the hash function.
type: str
choices: ['dst-ip', 'src-ip']
state:
description:
- The state the configuration should be left in.
type: str
choices:
- merged
- replaced
- overridden
- deleted
default: merged
"""
EXAMPLES = """
# Using merged
#
#
# ------------
# Before state
# ------------
#
# RP/0/0/CPU0:iosxr01#show run int
# Sun Jul 7 19:42:59.416 UTC
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description "GigabitEthernet - 1"
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# !
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# !
#
#
- name: Merge provided configuration with device configuration
iosxr_lag_interfaces:
config:
- name: Bundle-Ether10
members:
- member: GigabitEthernet0/0/0/1
mode: inherit
- member: GigabitEthernet0/0/0/3
mode: inherit
mode: active
links:
max_active: 5
min_active: 2
load_balancing_hash: src-ip
- name: Bundle-Ether12
members:
- member: GigabitEthernet0/0/0/2
mode: passive
- member: GigabitEthernet0/0/0/4
mode: passive
load_balancing_hash: dst-ip
state: merged
#
#
# -----------
# After state
# -----------
#
# RP/0/0/CPU0:iosxr01#show run int
# Sun Jul 7 20:51:17.685 UTC
# interface Bundle-Ether10
# lacp mode active
# bundle load-balancing hash src-ip
# bundle maximum-active links 5
# bundle minimum-active links 2
# !
# interface Bundle-Ether12
# bundle load-balancing hash dst-ip
# !
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description 'GigabitEthernet - 1"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# bundle id 12 mode passive
# !
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# bundle id 12 mode passive
# !
#
# Using replaced
#
#
# -------------
# Before state
# -------------
#
#
# RP/0/0/CPU0:iosxr01#sho run int
# Sun Jul 7 20:58:06.527 UTC
# interface Bundle-Ether10
# lacp mode active
# bundle load-balancing hash src-ip
# bundle maximum-active links 5
# bundle minimum-active links 2
# !
# interface Bundle-Ether12
# bundle load-balancing hash dst-ip
# !
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description 'GigabitEthernet - 1"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# bundle id 12 mode passive
# !
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# bundle id 12 mode passive
# !
#
#
- name: Replace device configuration of listed Bundles with provided configurations
iosxr_lag_interfaces:
config:
- name: Bundle-Ether12
members:
- name: GigabitEthernet0/0/0/2
mode: passive
- name: Bundle-Ether11
members:
- name: GigabitEthernet0/0/0/4
load_balancing_hash: src-ip
state: replaced
#
#
# -----------
# After state
# -----------
#
#
# RP/0/0/CPU0:iosxr01#sh run int
# Sun Jul 7 21:22:27.397 UTC
# interface Bundle-Ether10
# lacp mode active
# bundle load-balancing hash src-ip
# bundle maximum-active links 5
# bundle minimum-active links 2
# !
# interface Bundle-Ether11
# bundle load-balancing hash src-ip
# !
# interface Bundle-Ether12
# lacp mode passive
# !
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description 'GigabitEthernet - 1"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# bundle id 12 mode on
# !
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# bundle id 11 mode on
# !
#
#
# Using overridden
#
#
# ------------
# Before state
# ------------
#
#
# RP/0/0/CPU0:iosxr01#sh run int
# Sun Jul 7 21:22:27.397 UTC
# interface Bundle-Ether10
# lacp mode active
# bundle load-balancing hash src-ip
# bundle maximum-active links 5
# bundle minimum-active links 2
# !
# interface Bundle-Ether11
# bundle load-balancing hash src-ip
# !
# interface Bundle-Ether12
# lacp mode passive
# !
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description 'GigabitEthernet - 1"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# bundle id 12 mode on
# !
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# bundle id 11 mode on
# !
#
#
- name: Overrides all device configuration with provided configuration
iosxr_lag_interfaces:
config:
- name: Bundle-Ether10
members:
- member: GigabitEthernet0/0/0/1
mode: inherit
- member: GigabitEthernet0/0/0/2
mode: inherit
mode: active
load_balancing_hash: dst-ip
state: overridden
#
#
# ------------
# After state
# ------------
#
#
# RP/0/0/CPU0:iosxr01#sh run int
# Sun Jul 7 21:43:04.802 UTC
# interface Bundle-Ether10
# lacp mode active
# bundle load-balancing hash dst-ip
# !
# interface Bundle-Ether11
# !
# interface Bundle-Ether12
# !
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description 'GigabitEthernet - 1"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# !
#
#
# Using deleted
#
#
# ------------
# Before state
# ------------
#
# RP/0/0/CPU0:iosxr01#sh run int
# Sun Jul 7 21:22:27.397 UTC
# interface Bundle-Ether10
# lacp mode active
# bundle load-balancing hash src-ip
# bundle maximum-active links 5
# bundle minimum-active links 2
# !
# interface Bundle-Ether11
# bundle load-balancing hash src-ip
# !
# interface Bundle-Ether12
# lacp mode passive
# !
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description 'GigabitEthernet - 1"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# bundle id 12 mode on
# !n
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# bundle id 10 mode inherit
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# bundle id 11 mode on
# !
#
#
- name: Delete attributes of given bundles and removes member interfaces from them (Note - This won't delete the bundles themselves)
iosxr_lag_interfaces:
config:
- name: Bundle-Ether10
- name: Bundle-Ether11
- name: Bundle-Ether12
state: deleted
#
#
# ------------
# After state
# ------------
#
# RP/0/0/CPU0:iosxr01#sh run int
# Sun Jul 7 21:49:50.004 UTC
# interface Bundle-Ether10
# !
# interface Bundle-Ether11
# !
# interface Bundle-Ether12
# !
# interface Loopback888
# description test for ansible
# shutdown
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/1
# description 'GigabitEthernet - 1"
# !
# interface GigabitEthernet0/0/0/2
# description "GigabitEthernet - 2"
# !
# interface GigabitEthernet0/0/0/3
# description "GigabitEthernet - 3"
# !
# interface GigabitEthernet0/0/0/4
# description "GigabitEthernet - 4"
# !
#
#
# Using deleted (without config)
#
#
# ------------
# Before state
# ------------
#
# RP/0/0/CPU0:an-iosxr#sh run int
# Sun Aug 18 19:49:51.908 UTC
# interface Bundle-Ether10
# lacp mode active
# bundle load-balancing hash src-ip
# bundle maximum-active links 10
# bundle minimum-active links 2
# !
# interface Bundle-Ether11
# bundle load-balancing hash dst-ip
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 192.0.2.11 255.255.255.0
# !
# interface GigabitEthernet0/0/0/0
# shutdown
# !
# interface GigabitEthernet0/0/0/1
# bundle id 10 mode inherit
# shutdown
# !
# interface GigabitEthernet0/0/0/2
# bundle id 10 mode passive
# shutdown
# !
# interface GigabitEthernet0/0/0/3
# bundle id 11 mode passive
# shutdown
# !
# interface GigabitEthernet0/0/0/4
# bundle id 11 mode passive
# shutdown
# !
#
- name: Delete attributes of all bundles and removes member interfaces from them (Note - This won't delete the bundles themselves)
iosxr_lag_interfaces:
state: deleted
#
#
# ------------
# After state
# ------------
#
#
# RP/0/0/CPU0:an-iosxr#sh run int
# Sun Aug 18 19:54:22.389 UTC
# interface Bundle-Ether10
# !
# interface Bundle-Ether11
# !
# interface MgmtEth0/0/CPU0/0
# ipv4 address 10.8.38.69 255.255.255.0
# !
# interface GigabitEthernet0/0/0/0
# shutdown
# !
# interface GigabitEthernet0/0/0/1
# shutdown
# !
# interface GigabitEthernet0/0/0/2
# shutdown
# !
# interface GigabitEthernet0/0/0/3
# shutdown
# !
# interface GigabitEthernet0/0/0/4
# shutdown
# !
"""
RETURN = """
before:
description: The configuration prior to the model invocation.
returned: always
type: list
sample: >
The configuration returned will always be in the same format
of the parameters above.
after:
description: The resulting configuration model invocation.
returned: when changed
type: list
sample: >
The configuration returned will always be in the same format
of the parameters above.
commands:
description: The set of commands pushed to the remote device.
returned: always
type: list
sample: ['interface Bundle-Ether10', 'bundle minimum-active links 2', 'bundle load-balancing hash src-ip']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.iosxr.argspec.lag_interfaces.lag_interfaces import Lag_interfacesArgs
from ansible.module_utils.network.iosxr.config.lag_interfaces.lag_interfaces import Lag_interfaces
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
required_if = [('state', 'merged', ('config',)),
('state', 'replaced', ('config',)),
('state', 'overridden', ('config',))]
module = AnsibleModule(argument_spec=Lag_interfacesArgs.argument_spec, required_if=required_if,
supports_check_mode=True)
result = Lag_interfaces(module).execute_module()
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -190,7 +190,7 @@ class Cliconf(CliconfBase):
elif label: elif label:
cmd_obj['command'] = 'commit label {0}'.format(label) cmd_obj['command'] = 'commit label {0}'.format(label)
else: else:
cmd_obj['command'] = 'commit' cmd_obj['command'] = 'commit show-error'
self.send_command(**cmd_obj) self.send_command(**cmd_obj)

View file

@ -0,0 +1,3 @@
---
testcase: "[^_].*"
test_items: []

View file

@ -0,0 +1,20 @@
---
- name: Collect all cli test cases
find:
paths: "{{ role_path }}/tests/cli"
patterns: "{{ testcase }}.yaml"
use_regex: true
register: test_cases
delegate_to: localhost
- name: Set test_items
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
delegate_to: localhost
- name: Run test case (connection=network_cli)
include: "{{ test_case_to_run }}"
vars:
ansible_connection: network_cli
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,2 @@
---
- { include: cli.yaml, tags: ['cli'] }

View file

@ -0,0 +1,26 @@
---
- name: Setup
iosxr_lag_interfaces:
config:
- name: Bundle-Ether10
mode: active
members:
- member: GigabitEthernet0/0/0/0
mode: inherit
- member: GigabitEthernet0/0/0/1
mode: passive
load_balancing_hash: src-ip
links:
max_active: 10
min_active: 2
- name: Bundle-Ether11
load_balancing_hash: dst-ip
members:
- member: GigabitEthernet0/0/0/8
mode: passive
- member: GigabitEthernet0/0/0/9
mode: passive
state: merged

View file

@ -0,0 +1,34 @@
---
- name: Remove Bundles
cli_config:
config: "{{ lines }}"
vars:
lines: |
no interface Bundle-Ether10
no interface Bundle-Ether11
no interface Bundle-Ether12
ignore_errors: yes
- name: Remove LAG interface config
iosxr_config:
lines:
- no bundle id
- shutdown
parents: "interface GigabitEthernet{{ item }}"
loop:
- 0/0/0/0
- 0/0/0/1
- 0/0/0/2
- 0/0/0/8
- 0/0/0/9
ignore_errors: yes
- name: Remove unwanted interfaces from config
iosxr_config:
lines:
- "no interface GigabitEthernet{{ item }}"
loop:
- 0/0/0/2
- 0/0/0/8
- 0/0/0/9
ignore_errors: yes

View file

@ -0,0 +1,46 @@
---
- debug:
msg: "Start iosxr_lag_interfaces deleted integration tests ansible_connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Delete LAG interfaces configuration
iosxr_lag_interfaces: &deleted
state: deleted
register: result
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that the correct set of commands were generated
assert:
that:
- "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that the after dicts were correctly generated
assert:
that:
- "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Delete LACP attributes of all interfaces (IDEMPOTENT)
iosxr_lag_interfaces: *deleted
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result.changed == false"
- "result.commands|length == 0"
- name: Assert that the before dicts were correctly generated
assert:
that:
- "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,65 @@
---
- debug:
msg: "START iosxr_lag_interfaces merged integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- block:
- name: Merge the provided configuration with the exisiting running configuration
iosxr_lag_interfaces: &merged
config:
- name: Bundle-Ether10
mode: active
members:
- member: GigabitEthernet0/0/0/0
mode: inherit
- member: GigabitEthernet0/0/0/1
mode: passive
load_balancing_hash: src-ip
links:
max_active: 10
min_active: 2
- name: Bundle-Ether11
load_balancing_hash: dst-ip
members:
- member: GigabitEthernet0/0/0/8
mode: passive
- member: GigabitEthernet0/0/0/9
mode: passive
state: merged
register: result
- name: Assert that before dicts were correctly generated
assert:
that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that after dicts was correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
iosxr_lag_interfaces: *merged
register: result
- name: Assert that the previous task was idempotent
assert:
that:
- "result['changed'] == false"
- "result.commands|length == 0"
- name: Assert that before dicts were correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,59 @@
---
- debug:
msg: "START iosxr_lag_interfaces overridden integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Overridde all LAG interface configuration with provided configuration
iosxr_lag_interfaces: &overridden
config:
- name: Bundle-Ether11
mode: active
members:
- member: GigabitEthernet0/0/0/0
mode: active
- member: GigabitEthernet0/0/0/1
mode: active
load_balancing_hash: src-ip
links:
max_active: 10
min_active: 5
state: overridden
register: result
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that before dicts are correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Overridde all interface LAG interface configuration with provided configuration (IDEMPOTENT)
iosxr_lag_interfaces: *overridden
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
- "result.commands|length == 0"
- name: Assert that before dict is correctly generated
assert:
that:
- "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,56 @@
---
- debug:
msg: "START iosxr_lag_interfaces replaced integration tests on connection={{ ansible_connection }}"
- include_tasks: _remove_config.yaml
- include_tasks: _populate_config.yaml
- block:
- name: Replace device configurations of listed interfaces with provided configurations
iosxr_lag_interfaces: &replaced
config:
- name: Bundle-Ether10
mode: passive
members:
- member: GigabitEthernet0/0/0/0
mode: passive
load_balancing_hash: dst-ip
- name: Bundle-Ether12
mode: active
state: replaced
register: result
- name: Assert that correct set of commands were generated
assert:
that:
- "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
- name: Assert that before dicts are correctly generated
assert:
that:
- "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
- name: Assert that after dict is correctly generated
assert:
that:
- "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Replace device configurations of listed interfaces with provided configurarions (IDEMPOTENT)
iosxr_lag_interfaces: *replaced
register: result
- name: Assert that task was idempotent
assert:
that:
- "result['changed'] == false"
- "result.commands|length == 0"
- name: Assert that before dict is correctly generated
assert:
that:
- "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,62 @@
---
- debug:
msg: "START isoxr_lag_interfaces round trip integration tests on connection={{ ansible_connection }}"
- block:
- include_tasks: _remove_config.yaml
- name: Apply the provided configuration (base config)
iosxr_lag_interfaces:
config:
- name: Bundle-Ether10
members:
- member: GigabitEthernet0/0/0/1
mode: passive
links:
max_active: 10
min_active: 2
- name: Bundle-Ether11
mode: passive
state: merged
register: base_config
- name: Gather interfaces facts
iosxr_facts:
gather_subset:
- "!all"
- "!min"
gather_network_resources:
- lag_interfaces
- name: Apply the provided configuration (config to be reverted)
iosxr_lag_interfaces:
config:
- name: Bundle-Ether10
members:
- member: GigabitEthernet0/0/0/9
mode: active
- member: GigabitEthernet0/0/0/8
mode: passive
- name: Bundle-Ether11
mode: active
state: overridden
register: result
- name: Assert that changes were applied
assert:
that: "{{ round_trip['after'] | symmetric_difference(result['after']) |length == 0 }}"
- name: Revert back to base config using facts round trip
iosxr_lag_interfaces:
config: "{{ ansible_facts['network_resources']['lag_interfaces'] }}"
state: overridden
register: revert
- name: Assert that config was reverted
assert:
that: "{{ base_config['after'] | symmetric_difference(revert['after']) |length == 0 }}"
always:
- include_tasks: _remove_config.yaml

View file

@ -0,0 +1,149 @@
---
merged:
before: []
commands:
- "interface Bundle-Ether10"
- "bundle load-balancing hash src-ip"
- "bundle minimum-active links 2"
- "bundle maximum-active links 10"
- "lacp mode active"
- "interface GigabitEthernet0/0/0/1"
- "bundle id 10 mode passive"
- "interface GigabitEthernet0/0/0/0"
- "bundle id 10 mode inherit"
- "interface Bundle-Ether11"
- "bundle load-balancing hash dst-ip"
- "interface GigabitEthernet0/0/0/8"
- "bundle id 11 mode passive"
- "interface GigabitEthernet0/0/0/9"
- "bundle id 11 mode passive"
after:
- name: Bundle-Ether10
links:
max_active: 10
min_active: 2
load_balancing_hash: src-ip
members:
- member: GigabitEthernet0/0/0/0
mode: inherit
- member: GigabitEthernet0/0/0/1
mode: passive
mode: active
- name: Bundle-Ether11
load_balancing_hash: dst-ip
members:
- member: GigabitEthernet0/0/0/8
mode: passive
- member: GigabitEthernet0/0/0/9
mode: passive
replaced:
commands:
- interface Bundle-Ether10
- no bundle maximum-active links 10
- no bundle minimum-active links 2
- bundle load-balancing hash dst-ip
- lacp mode passive
- interface GigabitEthernet0/0/0/1
- no bundle id
- interface GigabitEthernet0/0/0/0
- bundle id 10 mode passive
- interface Bundle-Ether12
- lacp mode active
after:
- load_balancing_hash: dst-ip
members:
- member: GigabitEthernet0/0/0/0
mode: passive
mode: passive
name: Bundle-Ether10
- load_balancing_hash: dst-ip
members:
- member: GigabitEthernet0/0/0/8
mode: passive
- member: GigabitEthernet0/0/0/9
mode: passive
name: Bundle-Ether11
- mode: active
name: Bundle-Ether12
overridden:
commands:
- interface Bundle-Ether10
- no bundle maximum-active links 10
- no bundle minimum-active links 2
- no bundle load-balancing hash src-ip
- no lacp mode active
- interface GigabitEthernet0/0/0/0
- no bundle id
- interface GigabitEthernet0/0/0/1
- no bundle id
- interface Bundle-Ether11
- bundle load-balancing hash src-ip
- bundle minimum-active links 5
- bundle maximum-active links 10
- lacp mode active
- interface GigabitEthernet0/0/0/8
- no bundle id
- interface GigabitEthernet0/0/0/9
- no bundle id
- interface GigabitEthernet0/0/0/0
- bundle id 11 mode active
- interface GigabitEthernet0/0/0/1
- bundle id 11 mode active
after:
- name: Bundle-Ether10
- links:
max_active: 10
min_active: 5
load_balancing_hash: src-ip
members:
- member: GigabitEthernet0/0/0/0
mode: active
- member: GigabitEthernet0/0/0/1
mode: active
mode: active
name: Bundle-Ether11
deleted:
commands:
- interface Bundle-Ether10
- no bundle maximum-active links 10
- no bundle minimum-active links 2
- no bundle load-balancing hash src-ip
- no lacp mode active
- interface GigabitEthernet0/0/0/0
- no bundle id
- interface GigabitEthernet0/0/0/1
- no bundle id
- interface Bundle-Ether11
- no bundle load-balancing hash dst-ip
- interface GigabitEthernet0/0/0/8
- no bundle id
- interface GigabitEthernet0/0/0/9
- no bundle id
after:
- name: Bundle-Ether10
- name: Bundle-Ether11
round_trip:
after:
- members:
- member: GigabitEthernet0/0/0/8
mode: passive
- member: GigabitEthernet0/0/0/9
mode: active
name: Bundle-Ether10
- mode: active
name: Bundle-Ether11