New module eos_lldp_interfaces (#61341)

* Prepare existing files for lldp_interfaces

* Initial commit of scaffolding

* Implement facts

* Implement config

* Add tests

* Appease Shippable
This commit is contained in:
Nathaniel Case 2019-08-28 11:56:43 -04:00 committed by GitHub
parent 92d57796dd
commit 95ecf24b87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 808 additions and 1 deletions

View file

@ -23,6 +23,8 @@ CHOICES = [
'!lag_interfaces',
'lldp_global',
'!lldp_global',
'lldp_interfaces',
'!lldp_interfaces',
'vlans',
'!vlans',
]

View file

@ -0,0 +1,49 @@
#
# -*- 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_lldp_interfaces module
"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class Lldp_interfacesArgs(object):
"""The arg spec for the eos_lldp_interfaces module
"""
def __init__(self, **kwargs):
pass
argument_spec = {
'config': {
'elements': 'dict',
'options': {'name': {'type': 'str'},
'receive': {'type': 'bool'},
'transmit': {'type': 'bool'}},
'type': 'list'},
'state': {'choices': ['merged', 'replaced', 'overridden', 'deleted'],
'default': 'merged',
'type': 'str'}}

View file

@ -0,0 +1,213 @@
#
# -*- 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_lldp_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.cfg.base import ConfigBase
from ansible.module_utils.network.common.utils import to_list, dict_diff, param_list_to_dict
from ansible.module_utils.network.eos.facts.facts import Facts
class Lldp_interfaces(ConfigBase):
"""
The eos_lldp_interfaces class
"""
gather_subset = [
'!all',
'!min',
]
gather_network_resources = [
'lldp_interfaces',
]
def get_lldp_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)
lldp_interfaces_facts = facts['ansible_network_resources'].get('lldp_interfaces')
if not lldp_interfaces_facts:
return []
return lldp_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_lldp_interfaces_facts = self.get_lldp_interfaces_facts()
commands.extend(self.set_config(existing_lldp_interfaces_facts))
if commands:
if not self._module.check_mode:
self._connection.edit_config(commands)
result['changed'] = True
result['commands'] = commands
changed_lldp_interfaces_facts = self.get_lldp_interfaces_facts()
result['before'] = existing_lldp_interfaces_facts
if result['changed']:
result['after'] = changed_lldp_interfaces_facts
result['warnings'] = warnings
return result
def set_config(self, existing_lldp_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_lldp_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']
want = param_list_to_dict(want, remove_key=False)
have = param_list_to_dict(have, remove_key=False)
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 key, desired in want.items():
if key in have:
extant = have[key]
else:
extant = dict(name=key)
add_config = dict_diff(extant, desired)
del_config = dict_diff(desired, extant)
commands.extend(generate_commands(key, add_config, del_config))
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 key, extant in have.items():
if key in want:
desired = want[key]
else:
desired = dict(name=key)
add_config = dict_diff(extant, desired)
del_config = dict_diff(desired, extant)
commands.extend(generate_commands(key, add_config, del_config))
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 key, desired in want.items():
if key in have:
extant = have[key]
else:
extant = dict(name=key)
add_config = dict_diff(extant, desired)
commands.extend(generate_commands(key, add_config, {}))
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 key in want.keys():
desired = dict(name=key)
if key in have:
extant = have[key]
else:
extant = dict(name=key)
del_config = dict_diff(desired, extant)
commands.extend(generate_commands(key, {}, del_config))
return commands
def generate_commands(name, to_set, to_remove):
commands = []
for key, value in to_set.items():
if value is None:
continue
prefix = "" if value else "no "
commands.append("{0}lldp {1}".format(prefix, key))
for key in to_remove:
commands.append("lldp {0}".format(key))
if commands:
commands.insert(0, "interface {0}".format(name))
return commands

View file

@ -18,6 +18,7 @@ from ansible.module_utils.network.eos.facts.l3_interfaces.l3_interfaces import L
from ansible.module_utils.network.eos.facts.lacp.lacp import LacpFacts
from ansible.module_utils.network.eos.facts.lag_interfaces.lag_interfaces import Lag_interfacesFacts
from ansible.module_utils.network.eos.facts.lldp_global.lldp_global import Lldp_globalFacts
from ansible.module_utils.network.eos.facts.lldp_interfaces.lldp_interfaces import Lldp_interfacesFacts
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
@ -35,6 +36,7 @@ FACT_RESOURCE_SUBSETS = dict(
lacp=LacpFacts,
lag_interfaces=Lag_interfacesFacts,
lldp_global=Lldp_globalFacts,
lldp_interfaces=Lldp_interfacesFacts,
vlans=VlansFacts,
)

View file

@ -0,0 +1,93 @@
#
# -*- 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 lldp_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.eos.argspec.lldp_interfaces.lldp_interfaces import Lldp_interfacesArgs
class Lldp_interfacesFacts(object):
""" The eos lldp_interfaces fact class
"""
def __init__(self, module, subspec='config', options='options'):
self._module = module
self.argument_spec = Lldp_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 lldp_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('show running-config | section lldp')
# 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:
objs.append(obj)
ansible_facts['ansible_network_resources'].pop('lldp_interfaces', None)
facts = {}
if objs:
params = utils.validate_config(self.argument_spec, {'config': objs})
facts['lldp_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)
config['name'] = utils.parse_conf_arg(conf, 'interface')
matches = re.findall(r'(no )?lldp (\S+)', conf)
for match in matches:
config[match[1]] = not bool(match[0])
return utils.remove_empties(config)

View file

@ -51,7 +51,7 @@ options:
choices: [
'all', '!all', 'interfaces', '!interfaces', 'l2_interfaces', '!l2_interfaces',
'l3_interfaces', '!l3_interfaces', 'lacp', '!lacp', 'lag_interfaces', '!lag_interfaces',
'lldp_global', '!lldp_global', 'vlans', '!vlans',
'lldp_global', '!lldp_global', 'lldp_interfaces', '!lldp_interfaces', 'vlans', '!vlans',
]
version_added: "2.9"
"""

View file

@ -0,0 +1,248 @@
#!/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_lldp_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_lldp_interfaces
version_added: 2.9
short_description: Manage Link Layer Discovery Protocol (LLDP) attributes of interfaces on Arista EOS devices.
description:
- This module manages Link Layer Discovery Protocol (LLDP) attributes of interfaces on Arista EOS devices.
author: Nathaniel Case (@Qalthos)
options:
config:
description: A dictionary of LLDP interfaces options.
type: list
elements: dict
suboptions:
name:
description:
- Full name of the interface (i.e. Ethernet1).
type: str
receive:
description:
- Enable/disable LLDP RX on an interface.
type: bool
transmit:
description:
- Enable/disable LLDP TX on an interface.
type: bool
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 run | section ^interface
# interface Ethernet1
# no lldp receive
# interface Ethernet2
# no lldp transmit
- name: Merge provided configuration with running configuration
eos_lldp_interfaces:
config:
- name: Ethernet1
transmit: False
- name: Ethernet2
transmit: False
state: merged
#
# ------------
# After state
# ------------
#
# veos#show run | section ^interface
# interface Ethernet1
# no lldp transmit
# no lldp receive
# interface Ethernet2
# no lldp transmit
# Using replaced
#
#
# ------------
# Before state
# ------------
#
#
# veos#show run | section ^interface
# interface Ethernet1
# no lldp receive
# interface Ethernet2
# no lldp transmit
- name: Replace existing LLDP configuration of specified interfaces with provided configuration
eos_lldp_interfaces:
config:
- name: Ethernet1
transmit: False
state: replaced
#
# ------------
# After state
# ------------
#
# veos#show run | section ^interface
# interface Ethernet1
# no lldp transmit
# interface Ethernet2
# no lldp transmit
# Using overridden
#
#
# ------------
# Before state
# ------------
#
#
# veos#show run | section ^interface
# interface Ethernet1
# no lldp receive
# interface Ethernet2
# no lldp transmit
- name: Override the LLDP configuration of all the interfaces with provided configuration
eos_lldp_interfaces:
config:
- name: Ethernet1
transmit: False
state: overridden
#
# ------------
# After state
# ------------
#
# veos#show run | section ^interface
# interface Ethernet1
# no lldp transmit
# interface Ethernet2
# Using deleted
#
#
# ------------
# Before state
# ------------
#
#
# veos#show run | section ^interface
# interface Ethernet1
# no lldp receive
# interface Ethernet2
# no lldp transmit
- name: Delete LLDP configuration of specified interfaces (or all interfaces if none are specified)
eos_lldp_interfaces:
state: deleted
#
# ------------
# After state
# ------------
#
# veos#show run | section ^interface
# interface Ethernet1
# interface Ethernet2
"""
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 Ethernet1', 'no lldp transmit']
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.eos.argspec.lldp_interfaces.lldp_interfaces import Lldp_interfacesArgs
from ansible.module_utils.network.eos.config.lldp_interfaces.lldp_interfaces import Lldp_interfaces
def main():
"""
Main entry point for module execution
:returns: the result form module invocation
"""
module = AnsibleModule(argument_spec=Lldp_interfacesArgs.argument_spec,
supports_check_mode=True)
result = Lldp_interfaces(module).execute_module()
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,2 @@
---
testcase: "*"

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"
delegate_to: localhost
register: test_cases
- 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,36 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: Ethernet1
expected_config:
- name: Ethernet2
transmit: False
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- name: Returns interface lldp parameterss to default parameters
eos_lldp_interfaces:
config: "{{ config }}"
state: deleted
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.after) == []"
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces == expected_config"

View file

@ -0,0 +1,44 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: Ethernet1
transmit: False
- name: Ethernet2
transmit: False
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- name: Merge provided configuration with device configuration
eos_lldp_interfaces:
config: "{{ config }}"
state: merged
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.after) == []"
- set_fact:
expected_config:
- name: Ethernet1
transmit: False
receive: False
- name: Ethernet2
transmit: False
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.lldp_interfaces) == []"

View file

@ -0,0 +1,34 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: Ethernet1
transmit: False
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- name: Override the LLDP configuration of all interfaces with provided configuration
eos_lldp_interfaces:
config: "{{ config }}"
state: overridden
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.after) == []"
- assert:
that:
- "config|symmetric_difference(ansible_facts.network_resources.lldp_interfaces) == []"

View file

@ -0,0 +1,40 @@
---
- include_tasks: reset_config.yml
- set_fact:
config:
- name: Ethernet1
transmit: False
other_config:
- name: Ethernet2
transmit: False
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- name: Replace existing LLDP configuration of specified interfaces with provided configuration
eos_lldp_interfaces:
config: "{{ config }}"
state: replaced
register: result
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.before) == []"
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- assert:
that:
- "ansible_facts.network_resources.lldp_interfaces|symmetric_difference(result.after) == []"
- set_fact:
expected_config: "{{ config }} + {{ other_config }}"
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.lldp_interfaces) == []"

View file

@ -0,0 +1,26 @@
---
- name: Reset initial config
cli_config:
config: |
interface Ethernet1
no lldp receive
lldp transmit
interface Ethernet2
lldp receive
no lldp transmit
become: yes
- eos_facts:
gather_network_resources: lldp_interfaces
become: yes
- set_fact:
expected_config:
- name: Ethernet1
receive: False
- name: Ethernet2
transmit: False
- assert:
that:
- "expected_config|symmetric_difference(ansible_facts.network_resources.lldp_interfaces) == []"