diff options
author | Nathaniel Case <ncase@redhat.com> | 2019-08-13 10:09:52 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-13 10:09:52 -0400 |
commit | 6b5c7f7c42874d65762c0a4dd83a2d990e3d2db4 (patch) | |
tree | 4712a9d8d89327430cf1d85913a9793c1fdec62d | |
parent | ef0f28097e4e4f877059ac9e0baf97a64afae9f1 (diff) | |
download | ansible-6b5c7f7c42874d65762c0a4dd83a2d990e3d2db4.tar.gz |
Add new module eos_interfaces (#59729)
* Move module_utils
* Add eos_interfaces and deprecate eos_interface
* Add boilerplate, update ignores.txt
* Try to reconcile eos provider documentation with argspec
* Try to work around unknown interfaces
* Move param_list_to_dict to utils
30 files changed, 1300 insertions, 317 deletions
diff --git a/lib/ansible/module_utils/network/common/utils.py b/lib/ansible/module_utils/network/common/utils.py index 407797500e..074821daac 100644 --- a/lib/ansible/module_utils/network/common/utils.py +++ b/lib/ansible/module_utils/network/common/utils.py @@ -348,6 +348,26 @@ def dict_merge(base, other): return combined +def param_list_to_dict(param_list, unique_key="name", remove_key=True): + """Rotates a list of dictionaries to be a dictionary of dictionaries. + + :param param_list: The aforementioned list of dictionaries + :param unique_key: The name of a key which is present and unique in all of param_list's dictionaries. The value + behind this key will be the key each dictionary can be found at in the new root dictionary + :param remove_key: If True, remove unique_key from the individual dictionaries before returning. + """ + param_dict = {} + for params in param_list: + params = params.copy() + if remove_key: + name = params.pop(unique_key) + else: + name = params.get(unique_key) + param_dict[name] = params + + return param_dict + + def conditional(expr, val, cast=None): match = re.match(r'^(.+)\((.+)\)$', str(expr), re.I) if match: diff --git a/lib/ansible/module_utils/network/eos/argspec/__init__.py b/lib/ansible/module_utils/network/eos/argspec/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/argspec/__init__.py diff --git a/lib/ansible/module_utils/network/eos/argspec/facts/__init__.py b/lib/ansible/module_utils/network/eos/argspec/facts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/argspec/facts/__init__.py diff --git a/lib/ansible/module_utils/network/eos/argspec/facts/facts.py b/lib/ansible/module_utils/network/eos/argspec/facts/facts.py new file mode 100644 index 0000000000..0a4b5d2de8 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/argspec/facts/facts.py @@ -0,0 +1,29 @@ +# -*- 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 arg spec for the eos facts module. +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +CHOICES = [ + 'all', + '!all', + 'interfaces', + '!interfaces', +] + + +class FactsArgs(object): + """ The arg spec for the eos facts module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'gather_subset': dict(default=['!config'], type='list'), + 'gather_network_resources': dict(choices=CHOICES, type='list'), + } diff --git a/lib/ansible/module_utils/network/eos/argspec/interfaces/__init__.py b/lib/ansible/module_utils/network/eos/argspec/interfaces/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/argspec/interfaces/__init__.py diff --git a/lib/ansible/module_utils/network/eos/argspec/interfaces/interfaces.py b/lib/ansible/module_utils/network/eos/argspec/interfaces/interfaces.py new file mode 100644 index 0000000000..880e5126e7 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/argspec/interfaces/interfaces.py @@ -0,0 +1,50 @@ +# -*- 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_interfaces module +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class InterfacesArgs(object): + """The arg spec for the eos_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'options': { + 'name': {'required': True, 'type': 'str'}, + 'description': {'required': False, 'type': 'str'}, + 'enabled': {'default': True, 'required': False, 'type': 'bool'}, + 'mtu': {'required': False, 'type': 'int'}, + 'speed': {'required': False, 'type': 'str'}, + 'duplex': {'required': False, 'type': 'str'} + }, + 'type': 'list'}, + 'state': {'default': 'merged', 'choices': ['merged', 'replaced', 'overridden', 'deleted'], 'required': False, 'type': 'str'} + } diff --git a/lib/ansible/module_utils/network/eos/config/__init__.py b/lib/ansible/module_utils/network/eos/config/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/config/__init__.py diff --git a/lib/ansible/module_utils/network/eos/config/interfaces/__init__.py b/lib/ansible/module_utils/network/eos/config/interfaces/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/config/interfaces/__init__.py diff --git a/lib/ansible/module_utils/network/eos/config/interfaces/interfaces.py b/lib/ansible/module_utils/network/eos/config/interfaces/interfaces.py new file mode 100644 index 0000000000..cf3fba7e09 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/config/interfaces/interfaces.py @@ -0,0 +1,240 @@ +# -*- 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_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, param_list_to_dict + +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.eos.facts.facts import Facts + + +class Interfaces(ConfigBase): + """ + The eos_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'interfaces', + ] + + def get_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) + interfaces_facts = facts['ansible_network_resources'].get('interfaces') + if not interfaces_facts: + return [] + return 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_interfaces_facts = self.get_interfaces_facts() + commands.extend(self.set_config(existing_interfaces_facts)) + if commands: + if not self._module.check_mode: + self._connection.edit_config(commands) + result['changed'] = True + result['commands'] = commands + + changed_interfaces_facts = self.get_interfaces_facts() + + result['before'] = existing_interfaces_facts + if result['changed']: + result['after'] = changed_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_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_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 + """ + want = param_list_to_dict(want) + have = param_list_to_dict(have) + state = self._module.params['state'] + if state == 'overridden': + commands = state_overridden(want, have) + elif state == 'deleted': + commands = state_deleted(want, have) + elif state == 'merged': + commands = state_merged(want, have) + elif state == 'replaced': + commands = state_replaced(want, have) + return commands + + +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 = _compute_commands(want, have, replace=True, remove=True) + + replace = commands['replace'] + remove = commands['remove'] + + commands_by_interface = replace + for interface, commands in remove.items(): + commands_by_interface[interface] = replace.get(interface, []) + commands + + return _flatten_commands(commands_by_interface) + + +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 + """ + # Add empty desired state for unspecified interfaces + for key in have: + if key not in want: + want[key] = {} + + # Otherwise it's the same as replaced + return state_replaced(want, have) + + +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 = _compute_commands(want, have, replace=True) + return _flatten_commands(commands['replace']) + + +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 = _compute_commands(want, have, remove=True) + return _flatten_commands(commands['remove']) + + +def _compute_commands(want, have, replace=False, remove=False): + replace_params = {} + remove_params = {} + for name, config in want.items(): + extant = have.get(name, {}) + + if remove: + remove_params[name] = dict(set(extant.items()).difference(config.items())) + if replace: + replace_params[name] = dict(set(config.items()).difference(extant.items())) + if remove: + # We won't need to also clear the configuration if we've + # already set it to something + for param in replace_params[name]: + remove_params[name].pop(param, None) + + returns = {} + if replace: + returns['replace'] = _replace_config(replace_params) + if remove: + returns['remove'] = _remove_config(remove_params) + + return returns + + +def _remove_config(params): + """ + Generates commands to reset config to defaults based on keys provided. + """ + commands = {} + for interface, config in params.items(): + interface_commands = [] + for param in config: + if param == 'enabled': + interface_commands.append('no shutdown') + elif param in ('description', 'mtu'): + interface_commands.append('no {0}'.format(param)) + elif param == 'speed': + interface_commands.append('speed auto') + if interface_commands: + commands[interface] = interface_commands + + return commands + + +def _replace_config(params): + """ + Generates commands to replace config to new values based on provided dictionary. + """ + commands = {} + for interface, config in params.items(): + interface_commands = [] + for param, state in config.items(): + if param == 'description': + interface_commands.append('description "{0}"'.format(state)) + elif param == 'enabled': + interface_commands.append('{0}shutdown'.format('no ' if state else '')) + elif param == 'mtu': + interface_commands.append('mtu {0}'.format(state)) + if 'speed' in config: + interface_commands.append('speed {0}{1}'.format(config['speed'], config['duplex'])) + if interface_commands: + commands[interface] = interface_commands + + return commands + + +def _flatten_commands(command_dict): + commands = [] + for interface, interface_commands in command_dict.items(): + commands.append('interface {0}'.format(interface)) + commands.extend(interface_commands) + + return commands diff --git a/lib/ansible/module_utils/network/eos/eos.py b/lib/ansible/module_utils/network/eos/eos.py index 86364c832b..8a93d3e8d5 100644 --- a/lib/ansible/module_utils/network/eos/eos.py +++ b/lib/ansible/module_utils/network/eos/eos.py @@ -599,9 +599,12 @@ def is_json(cmd): def is_local_eapi(module): - transport = module.params['transport'] - provider_transport = (module.params['provider'] or {}).get('transport') - return 'eapi' in (transport, provider_transport) + transports = [] + transports.append(module.params.get('transport', "")) + provider = module.params.get('provider') + if provider: + transports.append(provider.get('transport', "")) + return 'eapi' in transports def to_command(module, commands): diff --git a/lib/ansible/module_utils/network/eos/facts/__init__.py b/lib/ansible/module_utils/network/eos/facts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/__init__.py diff --git a/lib/ansible/module_utils/network/eos/facts/facts.py b/lib/ansible/module_utils/network/eos/facts/facts.py new file mode 100644 index 0000000000..71b244200d --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/facts.py @@ -0,0 +1,53 @@ +# -*- 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 facts class for eos +this file validates each subset of facts and selectively +calls the appropriate facts gathering function +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +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.interfaces.interfaces import InterfacesArgs +from ansible.module_utils.network.eos.facts.interfaces.interfaces import InterfacesFacts +from ansible.module_utils.network.eos.facts.legacy.base import Default, Hardware, Config, Interfaces + + +FACT_LEGACY_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) +FACT_RESOURCE_SUBSETS = dict( + interfaces=InterfacesFacts, +) + + +class Facts(FactsBase): + """ The fact class for eos + """ + + VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) + VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) + + def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None): + """ Collect the facts for eos + :param legacy_facts_type: List of legacy facts types + :param resource_facts_type: List of resource fact types + :param data: previously collected conf + :rtype: dict + :return: the facts gathered + """ + netres_choices = FactsArgs.argument_spec['gather_network_resources'].get('choices', []) + if self.VALID_RESOURCE_SUBSETS: + self.get_network_resources_facts(netres_choices, FACT_RESOURCE_SUBSETS, resource_facts_type, data) + + if self.VALID_LEGACY_GATHER_SUBSETS: + self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type) + + return self.ansible_facts, self._warnings diff --git a/lib/ansible/module_utils/network/eos/facts/interfaces/__init__.py b/lib/ansible/module_utils/network/eos/facts/interfaces/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/interfaces/__init__.py diff --git a/lib/ansible/module_utils/network/eos/facts/interfaces/interfaces.py b/lib/ansible/module_utils/network/eos/facts/interfaces/interfaces.py new file mode 100644 index 0000000000..5cfe2a2891 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/interfaces/interfaces.py @@ -0,0 +1,103 @@ +# -*- 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 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.interfaces.interfaces import InterfacesArgs + + +class InterfacesFacts(object): + """ The eos interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = 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 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') + + # operate on a collection of resource x + config = data.split('interface ') + objs = [] + for conf in config: + if conf: + obj = self.render_config(self.generated_spec, conf) + if obj: + objs.append(obj) + facts = {} + if objs: + facts['interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['interfaces'].append(utils.remove_empties(cfg)) + 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) + + # populate the facts from the configuration + config['name'] = re.match(r'(\S+)', conf).group(1) + description = utils.parse_conf_arg(conf, 'description') + if description is not None: + config['description'] = description.replace('"', '') + shutdown = utils.parse_conf_cmd_arg(conf, 'shutdown', False) + config['enabled'] = shutdown if shutdown is False else True + config['mtu'] = utils.parse_conf_arg(conf, 'mtu') + + speed_pair = utils.parse_conf_arg(conf, 'speed') + if speed_pair: + state = speed_pair.split() + if state[0] == 'forced': + state = state[1] + else: + state = state[0] + + if state == 'auto': + config['duplex'] = state + else: + # remaining options are all e.g., 10half or 40gfull + config['speed'] = state[:-4] + config['duplex'] = state[-4:] + + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/eos/facts/legacy/__init__.py b/lib/ansible/module_utils/network/eos/facts/legacy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/legacy/__init__.py diff --git a/lib/ansible/module_utils/network/eos/facts/legacy/base.py b/lib/ansible/module_utils/network/eos/facts/legacy/base.py new file mode 100644 index 0000000000..50bb82e849 --- /dev/null +++ b/lib/ansible/module_utils/network/eos/facts/legacy/base.py @@ -0,0 +1,182 @@ +# -*- 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) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import platform +import re + +from ansible.module_utils.six import iteritems +from ansible.module_utils.network.eos.eos import run_commands, get_capabilities + + +class FactsBase(object): + + COMMANDS = frozenset() + + def __init__(self, module): + self.module = module + self.warnings = list() + self.facts = dict() + self.responses = None + + def populate(self): + self.responses = run_commands(self.module, list(self.COMMANDS), check_rc=False) + + +class Default(FactsBase): + + SYSTEM_MAP = { + 'serialNumber': 'serialnum', + } + + COMMANDS = [ + 'show version | json', + 'show hostname | json', + ] + + def populate(self): + super(Default, self).populate() + data = self.responses[0] + for key, value in iteritems(self.SYSTEM_MAP): + if key in data: + self.facts[value] = data[key] + + self.facts.update(self.responses[1]) + self.facts.update(self.platform_facts()) + + def platform_facts(self): + platform_facts = {} + + resp = get_capabilities(self.module) + device_info = resp['device_info'] + + platform_facts['system'] = device_info['network_os'] + + for item in ('model', 'image', 'version', 'platform', 'hostname'): + val = device_info.get('network_os_%s' % item) + if val: + platform_facts[item] = val + + platform_facts['api'] = resp['network_api'] + platform_facts['python_version'] = platform.python_version() + + return platform_facts + + +class Hardware(FactsBase): + + COMMANDS = [ + 'dir all-filesystems', + 'show version | json' + ] + + def populate(self): + super(Hardware, self).populate() + self.facts.update(self.populate_filesystems()) + self.facts.update(self.populate_memory()) + + def populate_filesystems(self): + data = self.responses[0] + + if isinstance(data, dict): + data = data['messages'][0] + + fs = re.findall(r'^Directory of (.+)/', data, re.M) + return dict(filesystems=fs) + + def populate_memory(self): + values = self.responses[1] + return dict( + memfree_mb=int(values['memFree']) / 1024, + memtotal_mb=int(values['memTotal']) / 1024 + ) + + +class Config(FactsBase): + + COMMANDS = ['show running-config'] + + def populate(self): + super(Config, self).populate() + self.facts['config'] = self.responses[0] + + +class Interfaces(FactsBase): + + INTERFACE_MAP = { + 'description': 'description', + 'physicalAddress': 'macaddress', + 'mtu': 'mtu', + 'bandwidth': 'bandwidth', + 'duplex': 'duplex', + 'lineProtocolStatus': 'lineprotocol', + 'interfaceStatus': 'operstatus', + 'forwardingModel': 'type' + } + + COMMANDS = [ + 'show interfaces | json', + 'show lldp neighbors | json' + ] + + def populate(self): + super(Interfaces, self).populate() + + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + + data = self.responses[0] + self.facts['interfaces'] = self.populate_interfaces(data) + + data = self.responses[1] + if data: + self.facts['neighbors'] = self.populate_neighbors(data['lldpNeighbors']) + + def populate_interfaces(self, data): + facts = dict() + for key, value in iteritems(data['interfaces']): + intf = dict() + + for remote, local in iteritems(self.INTERFACE_MAP): + if remote in value: + intf[local] = value[remote] + + if 'interfaceAddress' in value: + intf['ipv4'] = dict() + for entry in value['interfaceAddress']: + intf['ipv4']['address'] = entry['primaryIp']['address'] + intf['ipv4']['masklen'] = entry['primaryIp']['maskLen'] + self.add_ip_address(entry['primaryIp']['address'], 'ipv4') + + if 'interfaceAddressIp6' in value: + intf['ipv6'] = dict() + for entry in value['interfaceAddressIp6']['globalUnicastIp6s']: + intf['ipv6']['address'] = entry['address'] + intf['ipv6']['subnet'] = entry['subnet'] + self.add_ip_address(entry['address'], 'ipv6') + + facts[key] = intf + + return facts + + def add_ip_address(self, address, family): + if family == 'ipv4': + self.facts['all_ipv4_addresses'].append(address) + else: + self.facts['all_ipv6_addresses'].append(address) + + def populate_neighbors(self, neighbors): + facts = dict() + for value in neighbors: + port = value['port'] + if port not in facts: + facts[port] = list() + lldp = dict() + lldp['host'] = value['neighborDevice'] + lldp['port'] = value['neighborPort'] + facts[port].append(lldp) + return facts diff --git a/lib/ansible/modules/network/eos/eos_interface.py b/lib/ansible/modules/network/eos/_eos_interface.py index c6f1981607..c6f08ed63f 100644 --- a/lib/ansible/modules/network/eos/eos_interface.py +++ b/lib/ansible/modules/network/eos/_eos_interface.py @@ -9,7 +9,7 @@ __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], + 'status': ['deprecated'], 'supported_by': 'network'} @@ -22,6 +22,10 @@ short_description: Manage Interface on Arista EOS network devices description: - This module provides declarative management of Interfaces on Arista EOS network devices. +deprecated: + removed_in: "2.13" + alternative: eos_interfaces + why: Updated modules released with more functionality notes: - Tested against EOS 4.15 options: @@ -29,10 +33,12 @@ options: description: - Name of the Interface to be configured on remote device. The name of interface should be in expanded format and not abbreviated. + type: str required: true description: description: - Description of Interface upto 240 characters. + type: str enabled: description: - Interface link status. If the value is I(True) the interface state will be @@ -43,24 +49,29 @@ options: description: - This option configures autoneg and speed/duplex/flowcontrol for the interface given in C(name) option. + type: str mtu: description: - Set maximum transmission unit size in bytes of transmit packet for the interface given in C(name) option. + type: str tx_rate: description: - Transmit rate in bits per second (bps) for the interface given in C(name) option. - This is state check parameter only. - Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html) + type: str rx_rate: description: - Receiver rate in bits per second (bps) for the interface given in C(name) option. - This is state check parameter only. - Supports conditionals, see L(Conditionals in Networking Modules,../network/user_guide/network_working_with_command_output.html) + type: str neighbors: description: - Check the operational state of given interface C(name) for LLDP neighbor. - The following suboptions are available. + type: list suboptions: host: description: @@ -72,17 +83,20 @@ options: description: - List of Interfaces definitions. Each of the entry in aggregate list should define name of interface C(name) and other options as required. + type: list delay: description: - Time in seconds to wait before checking for the operational state on remote device. This wait is applicable for operational state argument which are I(state) with values C(up)/C(down), I(tx_rate) and I(rx_rate). default: 10 + type: int state: description: - State of the Interface configuration, C(up) means present and operationally up and C(down) means present and operationally C(down) default: present + type: str choices: ['present', 'absent', 'up', 'down'] extends_documentation_fragment: eos """ diff --git a/lib/ansible/modules/network/eos/eos_facts.py b/lib/ansible/modules/network/eos/eos_facts.py index efc3385d72..6d056f0dfb 100644 --- a/lib/ansible/modules/network/eos/eos_facts.py +++ b/lib/ansible/modules/network/eos/eos_facts.py @@ -1,20 +1,10 @@ #!/usr/bin/python -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. -# +# -*- 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) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], @@ -25,17 +15,17 @@ DOCUMENTATION = """ --- module: eos_facts version_added: "2.2" -author: "Peter Sprygada (@privateip)" +author: + - "Peter Sprygada (@privateip)" + - "Nathaniel Case (@Qalthos)" short_description: Collect facts from remote devices running Arista EOS description: - - Collects a base set of device facts from a remote device that - is running eos. This module prepends all of the - base network fact keys with C(ansible_net_<fact>). The facts - module will always collect a base set of facts from the device - and can enable or disable collection of additional facts. + - Collects facts from Arista devices running the EOS operating + system. This module places the facts gathered in the fact tree keyed by the + respective resource name. The facts module will always collect a + base set of facts from the device and can enable or disable + collection of additional facts. extends_documentation_fragment: eos -notes: - - Tested against EOS 4.15 options: gather_subset: description: @@ -46,23 +36,54 @@ options: with an initial C(M(!)) to specify that a specific subset should not be collected. required: false + type: list default: '!config' + gather_network_resources: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all and the resources like interfaces, vlans etc. + Can specify a list of values to include a larger subset. Values + can also be used with an initial C(M(!)) to specify that a + specific subset should not be collected. + required: false + choices: ['all', '!all', 'interfaces', '!interfaces'] + type: list + version_added: "2.9" """ EXAMPLES = """ -# Collect all facts from the device +- name: Gather all legacy facts - eos_facts: gather_subset: all -# Collect only the config and default facts -- eos_facts: +- name: Gather only the config and default facts + eos_facts: gather_subset: - config -# Do not collect hardware facts -- eos_facts: +- name: Do not gather hardware facts + eos_facts: gather_subset: - "!hardware" + +- name: Gather legacy and resource facts + eos_facts: + gather_subset: all + gather_network_resources: all + +- name: Gather only the interfaces resource facts and no legacy facts +- eos_facts: + gather_subset: + - '!all' + - '!min' + gather_network_resources: + - interfaces + +- name: Gather interfaces resource and minimal legacy facts + eos_facts: + gather_subset: min + gather_network_resources: interfaces """ RETURN = """ @@ -71,6 +92,11 @@ ansible_net_gather_subset: returned: always type: list +ansible_net_gather_network_resources: + description: The list of fact for network resource subsets collected from the device + returned: when the resource is configured + type: list + # default ansible_net_model: description: The model name returned from the device @@ -144,257 +170,30 @@ ansible_net_neighbors: type: dict """ -import platform -import re - from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.six import iteritems -from ansible.module_utils.network.eos.eos import run_commands, get_capabilities -from ansible.module_utils.network.eos.eos import eos_argument_spec, check_args - - -class FactsBase(object): - - COMMANDS = frozenset() - - def __init__(self, module): - self.module = module - self.facts = dict() - self.responses = None - - def populate(self): - self.responses = run_commands(self.module, list(self.COMMANDS), check_rc=False) - - -class Default(FactsBase): - - SYSTEM_MAP = { - 'serialNumber': 'serialnum', - } - - COMMANDS = [ - 'show version | json', - 'show hostname | json', - ] - - def populate(self): - super(Default, self).populate() - data = self.responses[0] - for key, value in iteritems(self.SYSTEM_MAP): - if key in data: - self.facts[value] = data[key] - - self.facts.update(self.responses[1]) - self.facts.update(self.platform_facts()) - - def platform_facts(self): - platform_facts = {} - - resp = get_capabilities(self.module) - device_info = resp['device_info'] - - platform_facts['system'] = device_info['network_os'] - - for item in ('model', 'image', 'version', 'platform', 'hostname'): - val = device_info.get('network_os_%s' % item) - if val: - platform_facts[item] = val - - platform_facts['api'] = resp['network_api'] - platform_facts['python_version'] = platform.python_version() - - return platform_facts - - -class Hardware(FactsBase): - - COMMANDS = [ - 'dir all-filesystems', - 'show version | json' - ] - - def populate(self): - super(Hardware, self).populate() - self.facts.update(self.populate_filesystems()) - self.facts.update(self.populate_memory()) - - def populate_filesystems(self): - data = self.responses[0] - - if isinstance(data, dict): - data = data['messages'][0] - - fs = re.findall(r'^Directory of (.+)/', data, re.M) - return dict(filesystems=fs) - - def populate_memory(self): - values = self.responses[1] - return dict( - memfree_mb=int(values['memFree']) / 1024, - memtotal_mb=int(values['memTotal']) / 1024 - ) - - -class Config(FactsBase): - - COMMANDS = ['show running-config'] - - def populate(self): - super(Config, self).populate() - self.facts['config'] = self.responses[0] - - -class Interfaces(FactsBase): - - INTERFACE_MAP = { - 'description': 'description', - 'physicalAddress': 'macaddress', - 'mtu': 'mtu', - 'bandwidth': 'bandwidth', - 'duplex': 'duplex', - 'lineProtocolStatus': 'lineprotocol', - 'interfaceStatus': 'operstatus', - 'forwardingModel': 'type' - } - - COMMANDS = [ - 'show interfaces | json', - 'show lldp neighbors | json' - ] - - def populate(self): - super(Interfaces, self).populate() - - self.facts['all_ipv4_addresses'] = list() - self.facts['all_ipv6_addresses'] = list() - - data = self.responses[0] - self.facts['interfaces'] = self.populate_interfaces(data) - - data = self.responses[1] - if data: - self.facts['neighbors'] = self.populate_neighbors(data['lldpNeighbors']) - - def populate_interfaces(self, data): - facts = dict() - for key, value in iteritems(data['interfaces']): - intf = dict() - - for remote, local in iteritems(self.INTERFACE_MAP): - if remote in value: - intf[local] = value[remote] - - if 'interfaceAddress' in value: - intf['ipv4'] = dict() - for entry in value['interfaceAddress']: - intf['ipv4']['address'] = entry['primaryIp']['address'] - intf['ipv4']['masklen'] = entry['primaryIp']['maskLen'] - self.add_ip_address(entry['primaryIp']['address'], 'ipv4') - - if 'interfaceAddressIp6' in value: - intf['ipv6'] = dict() - for entry in value['interfaceAddressIp6']['globalUnicastIp6s']: - intf['ipv6']['address'] = entry['address'] - intf['ipv6']['subnet'] = entry['subnet'] - self.add_ip_address(entry['address'], 'ipv6') - - facts[key] = intf - - return facts - - def add_ip_address(self, address, family): - if family == 'ipv4': - self.facts['all_ipv4_addresses'].append(address) - else: - self.facts['all_ipv6_addresses'].append(address) - - def populate_neighbors(self, neighbors): - facts = dict() - for value in neighbors: - port = value['port'] - if port not in facts: - facts[port] = list() - lldp = dict() - lldp['host'] = value['neighborDevice'] - lldp['port'] = value['neighborPort'] - facts[port].append(lldp) - return facts - - -FACT_SUBSETS = dict( - default=Default, - hardware=Hardware, - interfaces=Interfaces, - config=Config -) - -VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) +from ansible.module_utils.network.eos.argspec.facts.facts import FactsArgs +from ansible.module_utils.network.eos.facts.facts import Facts +from ansible.module_utils.network.eos.eos import eos_argument_spec def main(): - """main entry point for module execution - """ - argument_spec = dict( - gather_subset=dict(default=['!config'], type='list') - ) + """ Main entry point for module execution + :returns: ansible_facts + """ + argument_spec = FactsArgs.argument_spec argument_spec.update(eos_argument_spec) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - warnings = list() - check_args(module, warnings) - - gather_subset = module.params['gather_subset'] - - runable_subsets = set() - exclude_subsets = set() - - for subset in gather_subset: - if subset == 'all': - runable_subsets.update(VALID_SUBSETS) - continue - - if subset.startswith('!'): - subset = subset[1:] - if subset == 'all': - exclude_subsets.update(VALID_SUBSETS) - continue - exclude = True - else: - exclude = False - - if subset not in VALID_SUBSETS: - module.fail_json(msg='Subset must be one of [%s], got %s' % - (', '.join(VALID_SUBSETS), subset)) - - if exclude: - exclude_subsets.add(subset) - else: - runable_subsets.add(subset) - - if not runable_subsets: - runable_subsets.update(VALID_SUBSETS) - - runable_subsets.difference_update(exclude_subsets) - runable_subsets.add('default') - - facts = dict() - facts['gather_subset'] = list(runable_subsets) - - instances = list() - for key in runable_subsets: - instances.append(FACT_SUBSETS[key](module)) + warnings = ['default value for `gather_subset` ' + 'will be changed to `min` from `!config` v2.11 onwards'] - for inst in instances: - inst.populate() - facts.update(inst.facts) + result = Facts(module).get_facts() - ansible_facts = dict() - for key, value in iteritems(facts): - key = 'ansible_net_%s' % key - ansible_facts[key] = value + ansible_facts, additional_warnings = result + warnings.extend(additional_warnings) module.exit_json(ansible_facts=ansible_facts, warnings=warnings) diff --git a/lib/ansible/modules/network/eos/eos_interfaces.py b/lib/ansible/modules/network/eos/eos_interfaces.py new file mode 100644 index 0000000000..3e1dca9ffb --- /dev/null +++ b/lib/ansible/modules/network/eos/eos_interfaces.py @@ -0,0 +1,293 @@ +#!/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_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_interfaces +version_added: 2.9 +short_description: Manages interface attributes of Arista EOS interfaces +description: ['This module manages the interface attributes of Arista EOS interfaces.'] +author: ['Nathaniel Case (@qalthos)'] +options: + config: + description: The provided configuration + type: list + suboptions: + name: + description: + - Full name of the interface, e.g. GigabitEthernet1. + type: str + required: true + description: + description: + - Interface description + type: str + duplex: + description: + - Interface link status. Applicable for Ethernet interfaces only. + - Values other than C(auto) must also set I(speed). + - Ignored when I(speed) is set above C(1000). + type: str + enabled: + default: true + description: + - Administrative state of the interface. + - Set the value to C(true) to administratively enable the interface or C(false) + to disable it. + type: bool + mtu: + description: + - MTU for a specific interface. Must be an even number between 576 and 9216. + Applicable for Ethernet interfaces only. + type: int + speed: + description: + - Interface link speed. Applicable for Ethernet interfaces only. + type: str + state: + choices: + - merged + - replaced + - overridden + - deleted + default: merged + description: + - The state the configuration should be left in. + type: str + +""" + +EXAMPLES = """ +--- + +# Using merged + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Merge provided configuration with device configuration + eos_interfaces: + config: + - name: Ethernet1 + enabled: True + - name: Ethernet2 + description: 'Configured by Ansible' + enable: False + state: merged + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +# Using replaced + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Replaces device configuration of listed interfaces with provided configuration + eos_interfaces: + config: + - name: Ethernet1 + enabled: True + - name: Ethernet2 + description: 'Configured by Ansible' + enabled: False + state: replaced + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +# Using overridden + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Overrides all device configuration with provided configuration + eos_interfaces: + config: + - name: Ethernet1 + enabled: True + - name: Ethernet2 + description: 'Configured by Ansible' + enabled: False + state: overridden + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# description "Configured by Ansible" +# shutdown +# ! +# interface Management1 +# ip address dhcp +# ! + +# Using deleted + +# Before state: +# ------------- +# +# veos#show running-config | section interface +# interface Ethernet1 +# description "Interface 1" +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! + +- name: Delete or return interface parameters to default settings + eos_interfaces: + config: + - name: Ethernet1 + state: deleted + +# After state: +# ------------ +# +# veos#show running-config | section interface +# interface Ethernet1 +# ! +# interface Ethernet2 +# ! +# interface Management1 +# description "Management interface" +# ip address dhcp +# ! +""" + +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + 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: dict + 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 Ethernet2', 'shutdown', 'speed 10full'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.eos.argspec.interfaces.interfaces import InterfacesArgs +from ansible.module_utils.network.eos.config.interfaces.interfaces import Interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=InterfacesArgs.argument_spec, + supports_check_mode=True) + + result = Interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/doc_fragments/eos.py b/lib/ansible/plugins/doc_fragments/eos.py index 73435ae66a..e77bc921f0 100644 --- a/lib/ansible/plugins/doc_fragments/eos.py +++ b/lib/ansible/plugins/doc_fragments/eos.py @@ -55,11 +55,11 @@ options: port: description: - Specifies the port to use when building the connection to the remote - device. This value applies to either I(cli) or I(eapi). The port - value will default to the appropriate transport common port if - none is provided in the task. (cli=22, http=80, https=443). + device. This value applies to either I(cli) or I(eapi). + - The port value will default to the appropriate transport common port + if none is provided in the task (cli=22, http=80, https=443). type: int - default: 0 (use common port) + default: 0 username: description: - Configures the username to use to authenticate the connection to @@ -81,7 +81,6 @@ options: for either connecting or sending commands. If the timeout is exceeded before the operation is completed, the module will error. type: int - default: 10 ssh_keyfile: description: - Specifies the SSH keyfile to use to authenticate the connection to @@ -126,6 +125,7 @@ options: on personally controlled sites using self-signed certificates. If the transport argument is not eapi, this value is ignored. type: bool + default: true use_proxy: description: - If C(no), the environment variables C(http_proxy) and C(https_proxy) will be ignored. diff --git a/test/integration/targets/eos_facts/tests/cli/invalid_subset.yaml b/test/integration/targets/eos_facts/tests/cli/invalid_subset.yaml index b339fcfca6..d445b22adb 100644 --- a/test/integration/targets/eos_facts/tests/cli/invalid_subset.yaml +++ b/test/integration/targets/eos_facts/tests/cli/invalid_subset.yaml @@ -38,7 +38,7 @@ # It's a failure - "result.failed == true" # Sensible Failure message - #- "result.msg == 'Bad subset'" + - "'Subset must be one of' in result.msg" ignore_errors: true diff --git a/test/integration/targets/eos_interfaces/defaults/main.yaml b/test/integration/targets/eos_interfaces/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/eos_interfaces/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/eos_interfaces/meta/main.yml b/test/integration/targets/eos_interfaces/meta/main.yml new file mode 100644 index 0000000000..e5c8cd02f0 --- /dev/null +++ b/test/integration/targets/eos_interfaces/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_eos_tests diff --git a/test/integration/targets/eos_interfaces/tasks/main.yaml b/test/integration/targets/eos_interfaces/tasks/main.yaml new file mode 100644 index 0000000000..af64d48ed8 --- /dev/null +++ b/test/integration/targets/eos_interfaces/tasks/main.yaml @@ -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 diff --git a/test/integration/targets/eos_interfaces/tests/cli/deleted.yaml b/test/integration/targets/eos_interfaces/tests/cli/deleted.yaml new file mode 100644 index 0000000000..b447fb6b72 --- /dev/null +++ b/test/integration/targets/eos_interfaces/tests/cli/deleted.yaml @@ -0,0 +1,43 @@ +--- +- include_tasks: reset_config.yml + +- set_fact: + config: + - name: Ethernet1 + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- name: Returns interfaces to default parameters + eos_interfaces: + config: "{{ config }}" + state: deleted + register: result + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.before)|length == 0" + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.after)|length == 0" + +- set_fact: + expected_config: + - name: Ethernet1 + duplex: auto + enabled: True + - name: Ethernet2 + duplex: auto + enabled: True + mtu: "3000" + +- assert: + that: + - "expected_config|difference(ansible_facts.network_resources.interfaces)|length == 0" diff --git a/test/integration/targets/eos_interfaces/tests/cli/merged.yaml b/test/integration/targets/eos_interfaces/tests/cli/merged.yaml new file mode 100644 index 0000000000..8030f09d58 --- /dev/null +++ b/test/integration/targets/eos_interfaces/tests/cli/merged.yaml @@ -0,0 +1,53 @@ +--- +- include_tasks: reset_config.yml + +- set_fact: + config: + - name: Ethernet1 + enabled: True + - name: Ethernet2 + description: 'Configured by Ansible' + speed: '10' + duplex: full + enabled: False + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- name: Merge provided configuration with device configuration + eos_interfaces: + config: "{{ config }}" + state: merged + register: result + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.before)|length == 0" + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.after)|length == 0" + +- set_fact: + expected_config: + - name: Ethernet1 + description: Interface 1 + speed: 40g + duplex: full + enabled: True + - name: Ethernet2 + description: 'Configured by Ansible' + speed: '10' + duplex: full + enabled: False + mtu: "3000" + +- assert: + that: + - "expected_config|difference(ansible_facts.network_resources.interfaces)|length == 0" diff --git a/test/integration/targets/eos_interfaces/tests/cli/overridden.yaml b/test/integration/targets/eos_interfaces/tests/cli/overridden.yaml new file mode 100644 index 0000000000..b1b1777bfe --- /dev/null +++ b/test/integration/targets/eos_interfaces/tests/cli/overridden.yaml @@ -0,0 +1,41 @@ +--- +- include_tasks: reset_config.yml + +- set_fact: + config: + - name: Ethernet1 + duplex: auto + enabled: true + - name: Ethernet2 + duplex: auto + description: 'Configured by Ansible' + enabled: false + - name: Management1 + enabled: true + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- name: Overrides device configuration of all interfaces with provided configuration + eos_interfaces: + config: "{{ config }}" + state: overridden + register: result + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.before)|length == 0" + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.after)|length == 0" + +- assert: + that: + - "config|difference(ansible_facts.network_resources.interfaces)|length == 0" diff --git a/test/integration/targets/eos_interfaces/tests/cli/replaced.yaml b/test/integration/targets/eos_interfaces/tests/cli/replaced.yaml new file mode 100644 index 0000000000..ab9fd6fd2e --- /dev/null +++ b/test/integration/targets/eos_interfaces/tests/cli/replaced.yaml @@ -0,0 +1,39 @@ +--- +- include_tasks: reset_config.yml + +- set_fact: + config: + - name: Ethernet1 + duplex: auto + enabled: True + - name: Ethernet2 + description: 'Configured by Ansible' + duplex: auto + enabled: False + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- name: Replaces device configuration of listed interfaces with provided configuration + eos_interfaces: + config: "{{ config }}" + state: replaced + register: result + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.before)|length == 0" + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- assert: + that: + - "ansible_facts.network_resources.interfaces|symmetric_difference(result.after)|length == 0" + +- assert: + that: + - "config|difference(ansible_facts.network_resources.interfaces)|length == 0" diff --git a/test/integration/targets/eos_interfaces/tests/cli/reset_config.yml b/test/integration/targets/eos_interfaces/tests/cli/reset_config.yml new file mode 100644 index 0000000000..0613582955 --- /dev/null +++ b/test/integration/targets/eos_interfaces/tests/cli/reset_config.yml @@ -0,0 +1,35 @@ +--- +- name: Reset initial config + cli_config: + config: | + interface Ethernet1 + description "Interface 1" + no shutdown + no mtu + speed forced 40gfull + interface Ethernet2 + no description + no shutdown + mtu 3000 + speed auto + become: yes + +- eos_facts: + gather_network_resources: interfaces + become: yes + +- set_fact: + expected_config: + - name: Ethernet1 + description: Interface 1 + speed: 40g + duplex: full + enabled: True + - name: Ethernet2 + enabled: True + mtu: "3000" + duplex: auto + +- assert: + that: + - "expected_config|difference(ansible_facts.network_resources.interfaces) == []" diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 4831328dc6..f8159b967b 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -3556,8 +3556,6 @@ lib/ansible/modules/network/enos/enos_facts.py validate-modules:E337 lib/ansible/modules/network/enos/enos_facts.py validate-modules:E338 lib/ansible/modules/network/eos/eos_banner.py future-import-boilerplate lib/ansible/modules/network/eos/eos_banner.py metaclass-boilerplate -lib/ansible/modules/network/eos/eos_banner.py validate-modules:E324 -lib/ansible/modules/network/eos/eos_banner.py validate-modules:E327 lib/ansible/modules/network/eos/eos_banner.py validate-modules:E338 lib/ansible/modules/network/eos/eos_bgp.py validate-modules:E325 lib/ansible/modules/network/eos/eos_bgp.py validate-modules:E326 @@ -3565,105 +3563,73 @@ lib/ansible/modules/network/eos/eos_bgp.py validate-modules:E337 lib/ansible/modules/network/eos/eos_bgp.py validate-modules:E338 lib/ansible/modules/network/eos/eos_command.py future-import-boilerplate lib/ansible/modules/network/eos/eos_command.py metaclass-boilerplate -lib/ansible/modules/network/eos/eos_command.py validate-modules:E324 -lib/ansible/modules/network/eos/eos_command.py validate-modules:E327 lib/ansible/modules/network/eos/eos_command.py validate-modules:E337 lib/ansible/modules/network/eos/eos_command.py validate-modules:E338 lib/ansible/modules/network/eos/eos_config.py future-import-boilerplate lib/ansible/modules/network/eos/eos_config.py metaclass-boilerplate -lib/ansible/modules/network/eos/eos_config.py validate-modules:E324 -lib/ansible/modules/network/eos/eos_config.py validate-modules:E327 lib/ansible/modules/network/eos/eos_config.py validate-modules:E337 lib/ansible/modules/network/eos/eos_config.py validate-modules:E338 lib/ansible/modules/network/eos/eos_eapi.py future-import-boilerplate lib/ansible/modules/network/eos/eos_eapi.py metaclass-boilerplate lib/ansible/modules/network/eos/eos_eapi.py validate-modules:E324 -lib/ansible/modules/network/eos/eos_eapi.py validate-modules:E327 lib/ansible/modules/network/eos/eos_eapi.py validate-modules:E337 lib/ansible/modules/network/eos/eos_eapi.py validate-modules:E338 -lib/ansible/modules/network/eos/eos_facts.py future-import-boilerplate -lib/ansible/modules/network/eos/eos_facts.py metaclass-boilerplate -lib/ansible/modules/network/eos/eos_facts.py validate-modules:E324 -lib/ansible/modules/network/eos/eos_facts.py validate-modules:E327 -lib/ansible/modules/network/eos/eos_facts.py validate-modules:E337 -lib/ansible/modules/network/eos/eos_interface.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_interface.py validate-modules:E324 -lib/ansible/modules/network/eos/eos_interface.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_interface.py validate-modules:E327 -lib/ansible/modules/network/eos/eos_interface.py validate-modules:E337 -lib/ansible/modules/network/eos/eos_interface.py validate-modules:E338 -lib/ansible/modules/network/eos/eos_interface.py validate-modules:E340 +lib/ansible/modules/network/eos/_eos_interface.py validate-modules:E322 +lib/ansible/modules/network/eos/_eos_interface.py validate-modules:E326 +lib/ansible/modules/network/eos/_eos_interface.py validate-modules:E337 +lib/ansible/modules/network/eos/_eos_interface.py validate-modules:E338 +lib/ansible/modules/network/eos/_eos_interface.py validate-modules:E340 lib/ansible/modules/network/eos/eos_l2_interface.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_l2_interface.py validate-modules:E324 lib/ansible/modules/network/eos/eos_l2_interface.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_l2_interface.py validate-modules:E327 lib/ansible/modules/network/eos/eos_l2_interface.py validate-modules:E337 lib/ansible/modules/network/eos/eos_l2_interface.py validate-modules:E338 lib/ansible/modules/network/eos/eos_l2_interface.py validate-modules:E340 lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E324 lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_l3_interface.py validate-modules:E327 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:E340 lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E324 lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_linkagg.py validate-modules:E327 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:E340 -lib/ansible/modules/network/eos/eos_lldp.py validate-modules:E324 lib/ansible/modules/network/eos/eos_lldp.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_lldp.py validate-modules:E327 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 metaclass-boilerplate lib/ansible/modules/network/eos/eos_logging.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_logging.py validate-modules:E324 lib/ansible/modules/network/eos/eos_logging.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_logging.py validate-modules:E327 lib/ansible/modules/network/eos/eos_logging.py validate-modules:E337 lib/ansible/modules/network/eos/eos_logging.py validate-modules:E338 lib/ansible/modules/network/eos/eos_logging.py validate-modules:E340 lib/ansible/modules/network/eos/eos_static_route.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:E324 lib/ansible/modules/network/eos/eos_static_route.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_static_route.py validate-modules:E327 lib/ansible/modules/network/eos/eos_static_route.py validate-modules:E337 lib/ansible/modules/network/eos/eos_static_route.py validate-modules:E338 lib/ansible/modules/network/eos/eos_static_route.py validate-modules:E340 lib/ansible/modules/network/eos/eos_system.py future-import-boilerplate lib/ansible/modules/network/eos/eos_system.py metaclass-boilerplate -lib/ansible/modules/network/eos/eos_system.py validate-modules:E324 -lib/ansible/modules/network/eos/eos_system.py validate-modules:E327 lib/ansible/modules/network/eos/eos_system.py validate-modules:E337 lib/ansible/modules/network/eos/eos_system.py validate-modules:E338 lib/ansible/modules/network/eos/eos_user.py future-import-boilerplate lib/ansible/modules/network/eos/eos_user.py metaclass-boilerplate lib/ansible/modules/network/eos/eos_user.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_user.py validate-modules:E324 lib/ansible/modules/network/eos/eos_user.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_user.py validate-modules:E327 lib/ansible/modules/network/eos/eos_user.py validate-modules:E337 lib/ansible/modules/network/eos/eos_user.py validate-modules:E338 lib/ansible/modules/network/eos/eos_user.py validate-modules:E340 lib/ansible/modules/network/eos/eos_vlan.py future-import-boilerplate lib/ansible/modules/network/eos/eos_vlan.py metaclass-boilerplate lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E324 lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E327 lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E337 lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E338 lib/ansible/modules/network/eos/eos_vlan.py validate-modules:E340 lib/ansible/modules/network/eos/eos_vrf.py future-import-boilerplate lib/ansible/modules/network/eos/eos_vrf.py metaclass-boilerplate lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E322 -lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E324 lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E326 -lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E327 lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E337 lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E338 lib/ansible/modules/network/eos/eos_vrf.py validate-modules:E340 |