New module eos_lag_interfaces (#60610)

* Copy module files

* Deprecate eos_linkagg

* Add tests
This commit is contained in:
Nathaniel Case 2019-08-19 15:34:20 -04:00 committed by GitHub
parent 7c704526f3
commit efa163a2e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 843 additions and 7 deletions

View file

@ -15,6 +15,8 @@ CHOICES = [
'!interfaces', '!interfaces',
'l2_interfaces', 'l2_interfaces',
'!l2_interfaces', '!l2_interfaces',
'lag_interfaces',
'!lag_interfaces',
'vlans', 'vlans',
'!vlans', '!vlans',
] ]

View file

@ -0,0 +1,55 @@
# -*- 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 eos_lag_interfaces module
"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class Lag_interfacesArgs(object):
"""The arg spec for the eos_lag_interfaces module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'config': {
'elements': 'dict',
'options': {
'name': {'required': True, 'type': 'str'},
'members': {
'elements': 'dict',
'options': {
'member': {'type': 'str'},
'mode': {'choices': ['active', 'on', 'passive'], 'type': 'str'},
},
'type': 'list',
},
},
'type': 'list'},
'state': {'default': 'merged', 'choices': ['merged', 'replaced', 'overridden', 'deleted'], 'type': 'str'}
}

View file

@ -0,0 +1,220 @@
# -*- 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 eos_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 ansible.module_utils.network.common.utils import to_list, dict_diff
from ansible.module_utils.network.common.cfg.base import ConfigBase
from ansible.module_utils.network.eos.facts.facts import Facts
class Lag_interfaces(ConfigBase):
"""
The eos_lag_interfaces class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'lag_interfaces',
]
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}
commands = list()
warnings = 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']
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']
if state == 'overridden':
commands = self._state_overridden(want, have)
elif state == 'deleted':
commands = self._state_deleted(want, have)
elif state == 'merged':
commands = self._state_merged(want, have)
elif state == 'replaced':
commands = self._state_replaced(want, have)
return commands
@staticmethod
def _state_replaced(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 = []
for interface in want:
for extant in have:
if extant["name"] == interface["name"]:
break
else:
extant = dict(name=interface["name"])
commands.extend(set_config(interface, extant))
commands.extend(remove_config(interface, extant))
return commands
@staticmethod
def _state_overridden(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 extant in have:
for interface in want:
if interface["name"] == extant["name"]:
break
else:
interface = dict(name=extant["name"])
commands.extend(remove_config(interface, extant))
for interface in want:
for extant in have:
if extant["name"] == interface["name"]:
break
else:
extant = dict(name=interface["name"])
commands.extend(set_config(interface, extant))
return commands
@staticmethod
def _state_merged(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 = []
for interface in want:
for extant in have:
if extant["name"] == interface["name"]:
break
else:
extant = dict(name=interface["name"])
commands.extend(set_config(interface, extant))
return commands
@staticmethod
def _state_deleted(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 = []
for interface in want:
for extant in have:
if extant["name"] == interface["name"]:
break
else:
continue
# Clearing all args, send empty dictionary
interface = dict(name=interface["name"])
commands.extend(remove_config(interface, extant))
return commands
def set_config(want, have):
commands = []
to_set = dict_diff(have, want)
for member in to_set.get("members", []):
channel_id = want["name"][12:]
commands.extend([
"interface {0}".format(member["member"]),
"channel-group {0} mode {1}".format(channel_id, member["mode"]),
])
return commands
def remove_config(want, have):
commands = []
if not want.get("members"):
return ["no interface {0}".format(want["name"])]
to_remove = dict_diff(want, have)
for member in to_remove.get("members", []):
commands.extend([
"interface {0}".format(member["member"]),
"no channel-group",
])
return commands

View file

@ -14,6 +14,7 @@ from ansible.module_utils.network.common.facts.facts import FactsBase
from ansible.module_utils.network.eos.argspec.facts.facts import FactsArgs from ansible.module_utils.network.eos.argspec.facts.facts import FactsArgs
from ansible.module_utils.network.eos.facts.interfaces.interfaces import InterfacesFacts from ansible.module_utils.network.eos.facts.interfaces.interfaces import InterfacesFacts
from ansible.module_utils.network.eos.facts.l2_interfaces.l2_interfaces import L2_interfacesFacts from ansible.module_utils.network.eos.facts.l2_interfaces.l2_interfaces import L2_interfacesFacts
from ansible.module_utils.network.eos.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts
from ansible.module_utils.network.eos.facts.vlans.vlans import VlansFacts from ansible.module_utils.network.eos.facts.vlans.vlans import VlansFacts
from ansible.module_utils.network.eos.facts.legacy.base import Default, Hardware, Config, Interfaces from ansible.module_utils.network.eos.facts.legacy.base import Default, Hardware, Config, Interfaces
@ -27,6 +28,7 @@ FACT_LEGACY_SUBSETS = dict(
FACT_RESOURCE_SUBSETS = dict( FACT_RESOURCE_SUBSETS = dict(
interfaces=InterfacesFacts, interfaces=InterfacesFacts,
l2_interfaces=L2_interfacesFacts, l2_interfaces=L2_interfacesFacts,
lag_interfaces=Lag_interfacesFacts,
vlans=VlansFacts, vlans=VlansFacts,
) )

View file

@ -0,0 +1,102 @@
# -*- 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 eos 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
from copy import deepcopy
import re
from ansible.module_utils.network.common import utils
from ansible.module_utils.network.eos.argspec.lag_interfaces.lag_interfaces import Lag_interfacesArgs
class Lag_interfacesFacts(object):
""" The eos 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 data: previously collected configuration
:rtype: dictionary
:returns: facts
"""
if not data:
data = connection.get('show running-config | section ^interface')
# split the config into instances of the resource
resource_delim = 'interface'
find_pattern = r'(?:^|\n)%s.*?(?=(?:^|\n)%s|$)' % (resource_delim,
resource_delim)
resources = [p.strip() for p in re.findall(find_pattern,
data,
re.DOTALL)]
objs = {}
for resource in resources:
if resource:
obj = self.render_config(self.generated_spec, resource)
if obj:
group_name = obj['name']
if group_name in objs and "members" in obj:
config = objs[group_name]
if "members" not in config:
config["members"] = []
objs[group_name]['members'].extend(obj['members'])
else:
objs[group_name] = obj
objs = list(objs.values())
facts = {}
if objs:
params = utils.validate_config(self.argument_spec, {'config': objs})
facts['lag_interfaces'] = [utils.remove_empties(cfg) for cfg in params['config']]
ansible_facts['ansible_network_resources'].update(facts)
return ansible_facts
def render_config(self, spec, conf):
"""
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)
interface_name = utils.parse_conf_arg(conf, 'interface')
if interface_name.startswith("Port-Channel"):
config["name"] = interface_name
return utils.remove_empties(config)
interface = {'member': interface_name}
match = re.match(r'.*channel-group (\d+) mode (\S+)', conf, re.MULTILINE | re.DOTALL)
if match:
config['name'], interface['mode'] = match.groups()
config["name"] = "Port-Channel" + config["name"]
config['members'] = [interface]
return utils.remove_empties(config)

View file

@ -9,7 +9,7 @@ __metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1', ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'], 'status': ['deprecated'],
'supported_by': 'network'} 'supported_by': 'network'}
DOCUMENTATION = """ DOCUMENTATION = """
@ -21,6 +21,10 @@ short_description: Manage link aggregation groups on Arista EOS network devices
description: description:
- This module provides declarative management of link aggregation groups - This module provides declarative management of link aggregation groups
on Arista EOS network devices. on Arista EOS network devices.
deprecated:
removed_in: "2.13"
alternative: eos_lag_interfaces
why: Updated modules released with more functionality
notes: notes:
- Tested against EOS 4.15 - Tested against EOS 4.15
options: options:

View file

@ -48,7 +48,7 @@ options:
specific subset should not be collected. specific subset should not be collected.
required: false required: false
type: list type: list
choices: ['all', '!all', 'interfaces', '!interfaces', 'l2_interfaces', '!l2_interfaces', 'vlans', '!vlans'] choices: ['all', '!all', 'interfaces', '!interfaces', 'l2_interfaces', '!l2_interfaces', 'lag_interfaces', '!lag_interfaces', 'vlans', '!vlans']
version_added: "2.9" version_added: "2.9"
""" """

View file

@ -0,0 +1,251 @@
#!/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 eos_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: eos_lag_interfaces
version_added: 2.9
short_description: Manages link aggregation groups on Arista EOS devices
description: This module manages attributes of link aggregation groups on Arista EOS devices.
author: Nathaniel Case (@Qalthos)
notes:
- 'Tested against vEOS v4.20.x'
- This module works with connection C(network_cli). See the
L(EOS Platform Options,../network/user_guide/platform_eos.html).
options:
config:
description: A list of link aggregation group configurations.
type: list
elements: dict
suboptions:
name:
description:
- Name of the port-channel interface of the link aggregation group (LAG) e.g., Port-Channel5.
type: str
required: True
members:
description:
- Ethernet interfaces that are part of the group.
type: list
elements: dict
suboptions:
member:
description:
- Name of ethernet interface that is a member of the LAG.
type: str
mode:
description:
- LAG mode for this interface.
type: str
choices:
- active
- "on"
- passive
state:
description:
- The state the configuration should be left in.
type: str
choices:
- merged
- replaced
- overridden
- deleted
default: merged
"""
EXAMPLES = """
---
# Using merged
# Before state:
# -------------
#
# veos#show running-config | section interface
# interface Ethernet1
# channel group 5 mode on
# interface Ethernet2
- name: Merge provided LAG attributes with existing device configuration
eos_lag_interfaces:
config:
- name: 5
members:
- member: Ethernet2
mode: on
state: merged
# After state:
# ------------
#
# veos#show running-config | section interface
# interface Ethernet1
# channel group 5 mode on
# interface Ethernet2
# channel group 5 mode on
# Using replaced
# Before state:
# -------------
#
# veos#show running-config | section interface
# interface Ethernet1
# channel group 5 mode on
# interface Ethernet2
- name: Replace all device configuration of specified LAGs with provided configuration
eos_lag_interfaces:
config:
- name: 5
members:
- member: Ethernet2
mode: on
state: replaced
# After state:
# ------------
#
# veos#show running-config | section interface
# interface Ethernet1
# interface Ethernet2
# channel group 5 mode on
# Using overridden
# Before state:
# -------------
#
# veos#show running-config | section interface
# interface Ethernet1
# channel group 5 mode on
# interface Ethernet2
- name: Override all device configuration of all LAG attributes with provided configuration
eos_lag_interfaces:
config:
- name: 10
members:
- member: Ethernet2
mode: on
state: overridden
# After state:
# ------------
#
# veos#show running-config | section interface
# interface Ethernet1
# interface Ethernet2
# channel group 10 mode on
# Using deleted
# Before state:
# -------------
#
# veos#show running-config | section interface
# interface Ethernet1
# channel group 5 mode on
# interface Ethernet2
# channel group 5 mode on
- name: Delete LAG attributes of the given interfaces.
eos_lag_interfaces:
config:
- name: 5
members:
- member: Ethernet1
state: deleted
# After state:
# ------------
#
# veos#show running-config | section interface
# interface Ethernet1
# interface Ethernet2
# channel group 5 mode on
"""
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: ['command 1', 'command 2', 'command 3']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.eos.argspec.lag_interfaces.lag_interfaces import Lag_interfacesArgs
from ansible.module_utils.network.eos.config.lag_interfaces.lag_interfaces import Lag_interfaces
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=Lag_interfacesArgs.argument_spec,
supports_check_mode=True)
result = Lag_interfaces(module).execute_module()
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

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

View file

@ -0,0 +1,2 @@
dependencies:
- prepare_eos_tests

View file

@ -0,0 +1,16 @@
---
- 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 }} ansible_connection=network_cli"
with_items: "{{ test_items }}"
loop_control:
loop_var: test_case_to_run

View file

@ -0,0 +1,29 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: "Port-Channel5"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- name: Delete EOS L3 interfaces as in given arguments.
eos_lag_interfaces:
config: "{{ config }}"
state: deleted
become: yes
register: result
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(result.before)|length == 0"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- assert:
that:
- "'lag_interfaces' not in ansible_facts.network_resources"

View file

@ -0,0 +1,45 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: "Port-Channel5"
members:
- member: Ethernet2
mode: "on"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- name: Merge provided configuration with device configuration.
eos_lag_interfaces:
config: "{{ config }}"
state: merged
become: yes
register: result
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(result.before)|length == 0"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(result.after)|length == 0"
- set_fact:
expected_config:
- name: "Port-Channel5"
members:
- member: Ethernet1
mode: "on"
- member: Ethernet2
mode: "on"
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(expected_config)|length == 0"

View file

@ -0,0 +1,36 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: "Port-Channel10"
members:
- member: Ethernet2
mode: "on"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- name: Override device configuration of all LAGs on device with provided configuration.
eos_lag_interfaces:
config: "{{ config }}"
state: overridden
become: yes
register: result
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(result.before)|length == 0"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(result.after)|length == 0"
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(config)|length == 0"

View file

@ -0,0 +1,44 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: "Port-Channel10"
members:
- member: Ethernet2
mode: "on"
other_config:
- name: "Port-Channel5"
members:
- member: Ethernet1
mode: "on"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- name: Replace device configuration of specified LAGs with provided configuration.
eos_lag_interfaces:
config: "{{ config }}"
state: replaced
become: yes
register: result
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(result.before)|length == 0"
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(result.after)|length == 0"
- set_fact:
expected_config: "{{ config }} + {{ other_config }}"
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(expected_config)|length == 0"

View file

@ -0,0 +1,25 @@
---
- name: Reset state
cli_config:
config: |
interface Ethernet1
channel-group 5 mode on
interface Ethernet2
no channel-group
no interface Port-Channel10
become: yes
- eos_facts:
gather_network_resources: lag_interfaces
become: yes
- set_fact:
expected_config:
- name: "Port-Channel5"
members:
- member: Ethernet1
mode: "on"
- assert:
that:
- "ansible_facts.network_resources.lag_interfaces|symmetric_difference(expected_config)|length == 0"

View file

@ -3587,11 +3587,11 @@ lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E326
lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E337 lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E337
lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E338 lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E338
lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E340 lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E340
lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E322 lib/ansible/modules/network/eos/_eos_linkagg.py validate-modules:E322
lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E326 lib/ansible/modules/network/eos/_eos_linkagg.py validate-modules:E326
lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E337 lib/ansible/modules/network/eos/_eos_linkagg.py validate-modules:E337
lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E338 lib/ansible/modules/network/eos/_eos_linkagg.py validate-modules:E338
lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E340 lib/ansible/modules/network/eos/_eos_linkagg.py validate-modules:E340
lib/ansible/modules/network/eos/eos_lldp.py validate-modules:E326 lib/ansible/modules/network/eos/eos_lldp.py validate-modules:E326
lib/ansible/modules/network/eos/eos_lldp.py validate-modules:E338 lib/ansible/modules/network/eos/eos_lldp.py validate-modules:E338
lib/ansible/modules/network/eos/eos_logging.py future-import-boilerplate lib/ansible/modules/network/eos/eos_logging.py future-import-boilerplate