From f890c9692fa8858e6ba532e84909b40a94aa9577 Mon Sep 17 00:00:00 2001 From: Chris Van Heuveln Date: Thu, 29 Aug 2019 00:06:10 -0400 Subject: [PATCH] nxos_bfd_interfaces: new module (#61407) * bfd_interfaces: initial commit * fix state methods, add more UT * Add integration tests, support for N6K * minor comment cleanups * lint 1 * lint 2 * lint 3 * lint 4 * lint 5 * retrigger shippable * retrigger shippable * PR review updates: /bfd_echo/echo/, updated tests * fix overridden logic and integration test --- .../nxos/argspec/bfd_interfaces/__init__.py | 0 .../argspec/bfd_interfaces/bfd_interfaces.py | 56 ++++ .../network/nxos/argspec/facts/facts.py | 1 + .../nxos/config/bfd_interfaces/__init__.py | 0 .../config/bfd_interfaces/bfd_interfaces.py | 264 +++++++++++++++ .../nxos/facts/bfd_interfaces/__init__.py | 0 .../facts/bfd_interfaces/bfd_interfaces.py | 97 ++++++ .../module_utils/network/nxos/facts/facts.py | 2 + .../network/nxos/nxos_bfd_interfaces.py | 173 ++++++++++ .../modules/network/nxos/nxos_facts.py | 13 +- .../nxos_bfd_interfaces/defaults/main.yaml | 2 + .../targets/nxos_bfd_interfaces/meta/main.yml | 2 + .../nxos_bfd_interfaces/tasks/cli.yaml | 20 ++ .../nxos_bfd_interfaces/tasks/main.yaml | 2 + .../nxos_bfd_interfaces/tasks/nxapi.yaml | 27 ++ .../tests/cli/deleted.yaml | 71 ++++ .../nxos_bfd_interfaces/tests/cli/merged.yaml | 68 ++++ .../tests/cli/overridden.yaml | 67 ++++ .../tests/cli/replaced.yaml | 65 ++++ .../network/nxos/test_nxos_bfd_interfaces.py | 303 ++++++++++++++++++ 20 files changed, 1232 insertions(+), 1 deletion(-) create mode 100644 lib/ansible/module_utils/network/nxos/argspec/bfd_interfaces/__init__.py create mode 100644 lib/ansible/module_utils/network/nxos/argspec/bfd_interfaces/bfd_interfaces.py create mode 100644 lib/ansible/module_utils/network/nxos/config/bfd_interfaces/__init__.py create mode 100644 lib/ansible/module_utils/network/nxos/config/bfd_interfaces/bfd_interfaces.py create mode 100644 lib/ansible/module_utils/network/nxos/facts/bfd_interfaces/__init__.py create mode 100644 lib/ansible/module_utils/network/nxos/facts/bfd_interfaces/bfd_interfaces.py create mode 100644 lib/ansible/modules/network/nxos/nxos_bfd_interfaces.py create mode 100644 test/integration/targets/nxos_bfd_interfaces/defaults/main.yaml create mode 100644 test/integration/targets/nxos_bfd_interfaces/meta/main.yml create mode 100644 test/integration/targets/nxos_bfd_interfaces/tasks/cli.yaml create mode 100644 test/integration/targets/nxos_bfd_interfaces/tasks/main.yaml create mode 100644 test/integration/targets/nxos_bfd_interfaces/tasks/nxapi.yaml create mode 100644 test/integration/targets/nxos_bfd_interfaces/tests/cli/deleted.yaml create mode 100644 test/integration/targets/nxos_bfd_interfaces/tests/cli/merged.yaml create mode 100644 test/integration/targets/nxos_bfd_interfaces/tests/cli/overridden.yaml create mode 100644 test/integration/targets/nxos_bfd_interfaces/tests/cli/replaced.yaml create mode 100644 test/units/modules/network/nxos/test_nxos_bfd_interfaces.py diff --git a/lib/ansible/module_utils/network/nxos/argspec/bfd_interfaces/__init__.py b/lib/ansible/module_utils/network/nxos/argspec/bfd_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/argspec/bfd_interfaces/bfd_interfaces.py b/lib/ansible/module_utils/network/nxos/argspec/bfd_interfaces/bfd_interfaces.py new file mode 100644 index 00000000000..3c41030fe93 --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/argspec/bfd_interfaces/bfd_interfaces.py @@ -0,0 +1,56 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Cisco and/or its affiliates. +# 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. +# +############################################# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +""" +The arg spec for the nxos_bfd_interfaces module +""" + + +class Bfd_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the nxos_bfd_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'elements': 'dict', + 'options': { + 'name': {'type': 'str'}, + 'bfd': { + 'choices': ['enable', 'disable'], 'type': 'str'}, + 'echo': { + 'choices': ['enable', 'disable'], 'type': 'str'}, + }, + 'type': 'list' + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py b/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py index 99123c173cc..8ddd4ecddb6 100644 --- a/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py +++ b/lib/ansible/module_utils/network/nxos/argspec/facts/facts.py @@ -9,6 +9,7 @@ The arg spec for the nxos facts module. CHOICES = [ 'all', + 'bfd_interfaces', 'lag_interfaces', 'lldp_global', 'telemetry', diff --git a/lib/ansible/module_utils/network/nxos/config/bfd_interfaces/__init__.py b/lib/ansible/module_utils/network/nxos/config/bfd_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/config/bfd_interfaces/bfd_interfaces.py b/lib/ansible/module_utils/network/nxos/config/bfd_interfaces/bfd_interfaces.py new file mode 100644 index 00000000000..35ff3a9cbbc --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/config/bfd_interfaces/bfd_interfaces.py @@ -0,0 +1,264 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Cisco and/or its affiliates. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +""" +nxos_bfd_interfaces class +This class creates a command set to bring the current device configuration +to a desired end-state. The command set is based on a comparison of the +current configuration (as dict) and the provided configuration (as dict). +""" +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re +from ansible.module_utils.network.common.cfg.base import ConfigBase +from ansible.module_utils.network.common.utils import dict_diff, to_list, remove_empties +from ansible.module_utils.network.nxos.facts.facts import Facts +from ansible.module_utils.network.nxos.utils.utils import flatten_dict, get_interface_type, normalize_interface, search_obj_in_list, vlan_range_to_list + + +class Bfd_interfaces(ConfigBase): + """ + The nxos_bfd_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + gather_network_resources = [ + 'bfd_interfaces', + ] + # exclude_params = [] + + def __init__(self, module): + super(Bfd_interfaces, self).__init__(module) + + def get_bfd_interfaces_facts(self): + """ Get the 'facts' (the current configuration) + + :returns: A list of interface configs and a platform string + """ + facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources) + bfd_interfaces_facts = facts['ansible_network_resources'].get('bfd_interfaces', []) + platform = facts.get('ansible_net_platform', '') + return bfd_interfaces_facts, platform + + def edit_config(self, commands): + return self._connection.edit_config(commands) + + def execute_module(self): + """ Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + result = {'changed': False} + warnings = list() + cmds = list() + + existing_bfd_interfaces_facts, platform = self.get_bfd_interfaces_facts() + cmds.extend(self.set_config(existing_bfd_interfaces_facts, platform)) + if cmds: + if not self._module.check_mode: + self.edit_config(cmds) + result['changed'] = True + result['commands'] = cmds + + changed_bfd_interfaces_facts, platform = self.get_bfd_interfaces_facts() + result['before'] = existing_bfd_interfaces_facts + if result['changed']: + result['after'] = changed_bfd_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_bfd_interfaces_facts, platform): + """ 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 + """ + if re.search('N[56]K', platform): + # Some platforms do not support the 'bfd' interface keyword; + # remove the 'bfd' key from each want/have interface. + orig_want = self._module.params['config'] + want = [] + for w in orig_want: + del w['bfd'] + want.append(w) + orig_have = existing_bfd_interfaces_facts + have = [] + for h in orig_have: + del h['bfd'] + have.append(h) + else: + want = self._module.params['config'] + have = existing_bfd_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 in ('overridden', 'merged', 'replaced') and not want: + self._module.fail_json(msg='config is required for state {0}'.format(state)) + + cmds = list() + if state == 'overridden': + cmds.extend(self._state_overridden(want, have)) + elif state == 'deleted': + cmds.extend(self._state_deleted(want, have)) + else: + for w in want: + if state == 'merged': + cmds.extend(self._state_merged(flatten_dict(w), have)) + elif state == 'replaced': + cmds.extend(self._state_replaced(flatten_dict(w), have)) + return cmds + + def _state_replaced(self, want, have): + """ The command generator when state is replaced + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + cmds = [] + obj_in_have = search_obj_in_list(want['name'], have, 'name') + if obj_in_have: + diff = dict_diff(want, obj_in_have) + else: + diff = want + merged_cmds = self.set_commands(want, have) + if 'name' not in diff: + diff['name'] = want['name'] + + replaced_cmds = [] + if obj_in_have: + replaced_cmds = self.del_attribs(diff) + if replaced_cmds or merged_cmds: + for cmd in set(replaced_cmds).intersection(set(merged_cmds)): + merged_cmds.remove(cmd) + cmds.extend(replaced_cmds) + cmds.extend(merged_cmds) + return cmds + + def _state_overridden(self, want, have): + """ The command generator when state is overridden + + :rtype: A list + :returns: the commands necessary to migrate the current configuration + to the desired configuration + """ + cmds = [] + for h in have: + # Clean up bfd attrs for any interfaces not listed in the play + h = flatten_dict(h) + obj_in_want = flatten_dict(search_obj_in_list(h['name'], want, 'name')) + if obj_in_want: + # Let the 'want' loop handle all vals for this interface + continue + cmds.extend(self.del_attribs(h)) + for w in want: + # Update any want attrs if needed. The overridden state considers + # the play as the source of truth for the entire device, therefore + # set any unspecified attrs to their default state. + w = self.set_none_vals_to_defaults(flatten_dict(w)) + cmds.extend(self.set_commands(w, have)) + return cmds + + def _state_merged(self, want, have): + """ The command generator when state is merged + + :rtype: A list + :returns: the commands necessary to merge the provided into + the current configuration + """ + return self.set_commands(want, have) + + def _state_deleted(self, want, have): + """ The command generator when state is deleted + + :rtype: A list + :returns: the commands necessary to remove the current configuration + of the provided objects + """ + if not (want or have): + return [] + cmds = [] + if want: + for w in want: + obj_in_have = flatten_dict(search_obj_in_list(w['name'], have, 'name')) + cmds.extend(self.del_attribs(obj_in_have)) + else: + for h in have: + cmds.extend(self.del_attribs(flatten_dict(h))) + return cmds + + def del_attribs(self, obj): + if not obj or len(obj.keys()) == 1: + return [] + cmds = [] + # 'bfd' and 'bfd echo' are enabled by default so the handling is + # counter-intuitive; we are enabling them to remove them. The end result + # is that they are removed from the interface config on the device. + if 'bfd' in obj and 'disable' in obj['bfd']: + cmds.append('bfd') + if 'echo' in obj and 'disable' in obj['echo']: + cmds.append('bfd echo') + if cmds: + cmds.insert(0, 'interface ' + obj['name']) + return cmds + + def set_none_vals_to_defaults(self, want): + # Set dict None values to default states + if 'bfd' in want and want['bfd'] is None: + want['bfd'] = 'enable' + if 'echo' in want and want['echo'] is None: + want['echo'] = 'enable' + return want + + def diff_of_dicts(self, want, obj_in_have): + diff = set(want.items()) - set(obj_in_have.items()) + diff = dict(diff) + if diff and want['name'] == obj_in_have['name']: + diff.update({'name': want['name']}) + return diff + + def add_commands(self, want): + if not want: + return [] + cmds = [] + if 'bfd' in want and want['bfd'] is not None: + cmd = 'bfd' if want['bfd'] == 'enable' else 'no bfd' + cmds.append(cmd) + if 'echo' in want and want['echo'] is not None: + cmd = 'bfd echo' if want['echo'] == 'enable' else 'no bfd echo' + cmds.append(cmd) + + if cmds: + cmds.insert(0, 'interface ' + want['name']) + return cmds + + def set_commands(self, want, have): + cmds = [] + obj_in_have = flatten_dict(search_obj_in_list(want['name'], have, 'name')) + if not obj_in_have: + cmds = self.add_commands(want) + else: + diff = self.diff_of_dicts(want, obj_in_have) + cmds = self.add_commands(diff) + return cmds diff --git a/lib/ansible/module_utils/network/nxos/facts/bfd_interfaces/__init__.py b/lib/ansible/module_utils/network/nxos/facts/bfd_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/facts/bfd_interfaces/bfd_interfaces.py b/lib/ansible/module_utils/network/nxos/facts/bfd_interfaces/bfd_interfaces.py new file mode 100644 index 00000000000..ad1c63a8597 --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/facts/bfd_interfaces/bfd_interfaces.py @@ -0,0 +1,97 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2019 Cisco and/or its affiliates. +# 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 + +""" +The nxos bfd_interfaces fact class +Populate the facts tree based on the current device configuration. +""" +import re +from copy import deepcopy + +from ansible.module_utils.network.common import utils +from ansible.module_utils.network.nxos.argspec.bfd_interfaces.bfd_interfaces import Bfd_interfacesArgs +from ansible.module_utils.network.nxos.utils.utils import get_interface_type + + +class Bfd_interfacesFacts(object): + """ The nxos_bfd_interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Bfd_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 bfd_interfaces + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + :rtype: dictionary + :returns: facts + """ + objs = [] + + if not data: + data = connection.get("show running-config | section '^interface|^feature bfd'") + + # Some of the bfd attributes + if 'feature bfd' in data.split('\n'): + resources = data.split('interface ') + resources.pop(0) + else: + resources = [] + for resource in resources: + if resource: + obj = self.render_config(self.generated_spec, resource) + if obj and len(obj.keys()) > 1: + objs.append(obj) + + ansible_facts['ansible_network_resources'].pop('bfd_interfaces', None) + facts = {} + if objs: + facts['bfd_interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['bfd_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) + + match = re.search(r'^(\S+)', conf) + intf = match.group(1) + if get_interface_type(intf) == 'unknown': + return {} + config['name'] = intf + # 'bfd'/'bfd echo' do not nvgen when enabled thus set to 'enable' when None. + # 'bfd' is not supported on some platforms + config['bfd'] = utils.parse_conf_cmd_arg(conf, 'bfd', 'enable', 'disable') or 'enable' + config['echo'] = utils.parse_conf_cmd_arg(conf, 'bfd echo', 'enable', 'disable') or 'enable' + + return utils.remove_empties(config) diff --git a/lib/ansible/module_utils/network/nxos/facts/facts.py b/lib/ansible/module_utils/network/nxos/facts/facts.py index 1b1f49a49e8..80abb343acb 100644 --- a/lib/ansible/module_utils/network/nxos/facts/facts.py +++ b/lib/ansible/module_utils/network/nxos/facts/facts.py @@ -12,6 +12,7 @@ calls the appropriate facts gathering function from ansible.module_utils.network.nxos.argspec.facts.facts import FactsArgs from ansible.module_utils.network.common.facts.facts import FactsBase from ansible.module_utils.network.nxos.facts.legacy.base import Default, Legacy, Hardware, Config, Interfaces, Features +from ansible.module_utils.network.nxos.facts.bfd_interfaces.bfd_interfaces import Bfd_interfacesFacts from ansible.module_utils.network.nxos.facts.interfaces.interfaces import InterfacesFacts from ansible.module_utils.network.nxos.facts.l2_interfaces.l2_interfaces import L2_interfacesFacts from ansible.module_utils.network.nxos.facts.lacp.lacp import LacpFacts @@ -32,6 +33,7 @@ FACT_LEGACY_SUBSETS = dict( features=Features, ) FACT_RESOURCE_SUBSETS = dict( + bfd_interfaces=Bfd_interfacesFacts, lag_interfaces=Lag_interfacesFacts, lldp_global=Lldp_globalFacts, telemetry=TelemetryFacts, diff --git a/lib/ansible/modules/network/nxos/nxos_bfd_interfaces.py b/lib/ansible/modules/network/nxos/nxos_bfd_interfaces.py new file mode 100644 index 00000000000..530910ef1a0 --- /dev/null +++ b/lib/ansible/modules/network/nxos/nxos_bfd_interfaces.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Cisco and/or its affiliates. +# 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 nxos_bfd_interfaces +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network' +} + +DOCUMENTATION = """ +--- +module: nxos_bfd_interfaces +version_added: 2.9 +short_description: 'Manages BFD attributes of nxos interfaces.' +description: 'Manages attributes of Bidirectional Forwarding Detection (BFD) on the interface.' +author: Chris Van Heuveln (@chrisvanheuveln) +notes: +options: + config: + description: The provided configuration + type: list + elements: dict + suboptions: + name: + type: str + description: The name of the interface. + bfd: + type: str + description: + - Enable/Disable Bidirectional Forwarding Detection (BFD) on the interface. + choices: + - enable + - disable + echo: + type: str + description: + - Enable/Disable BFD Echo functionality on the interface. + choices: + - enable + - disable + state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - overridden + - deleted + default: merged +""" +EXAMPLES = """ +# Using deleted + +- name: Configure interfaces + nxos_bfd_interfaces: + operation: deleted + + +# Using merged + +- name: Configure interfaces + nxos_bfd_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + echo: enable + - name: Ethernet1/2 + bfd: disable + echo: disable + operation: merged + + +# Using overridden + +- name: Configure interfaces + nxos_bfd_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + echo: enable + - name: Ethernet1/2 + bfd: disable + echo: disable + operation: overridden + + +# Using replaced + +- name: Configure interfaces + nxos_bfd_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + echo: enable + - name: Ethernet1/2 + bfd: disable + echo: disable + operation: replaced + + +""" +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/1', 'no bfd', 'no bfd echo'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.nxos.argspec.bfd_interfaces.bfd_interfaces import Bfd_interfacesArgs +from ansible.module_utils.network.nxos.config.bfd_interfaces.bfd_interfaces import Bfd_interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Bfd_interfacesArgs.argument_spec, + supports_check_mode=True) + + result = Bfd_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/nxos/nxos_facts.py b/lib/ansible/modules/network/nxos/nxos_facts.py index 4a44ab9c4d0..cf3fdd12aad 100644 --- a/lib/ansible/modules/network/nxos/nxos_facts.py +++ b/lib/ansible/modules/network/nxos/nxos_facts.py @@ -57,7 +57,18 @@ options: 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. - choices: ['all', 'lag_interfaces', 'telemetry', 'vlans', 'lacp', 'lacp_interfaces', 'interfaces', 'l3_interfaces', 'l2_interfaces', 'lldp_global'] + choices: + - all + - bfd_interfaces + - lag_interfaces + - telemetry + - vlans + - lacp + - lacp_interfaces + - interfaces + - l3_interfaces + - l2_interfaces + - lldp_global required: false version_added: "2.9" """ diff --git a/test/integration/targets/nxos_bfd_interfaces/defaults/main.yaml b/test/integration/targets/nxos_bfd_interfaces/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/nxos_bfd_interfaces/meta/main.yml b/test/integration/targets/nxos_bfd_interfaces/meta/main.yml new file mode 100644 index 00000000000..ae741cbdc71 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_nxos_tests diff --git a/test/integration/targets/nxos_bfd_interfaces/tasks/cli.yaml b/test/integration/targets/nxos_bfd_interfaces/tasks/cli.yaml new file mode 100644 index 00000000000..6c7ea4a7f94 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/tasks/cli.yaml @@ -0,0 +1,20 @@ +--- +- name: collect common test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + connection: local + register: test_cases + +- set_fact: + test_cases: + files: "{{ test_cases.files }}" + +- 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 connection={{ cli }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/nxos_bfd_interfaces/tasks/main.yaml b/test/integration/targets/nxos_bfd_interfaces/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/nxos_bfd_interfaces/tasks/nxapi.yaml b/test/integration/targets/nxos_bfd_interfaces/tasks/nxapi.yaml new file mode 100644 index 00000000000..cbf41b92947 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/tasks/nxapi.yaml @@ -0,0 +1,27 @@ +--- +- name: collect common test cases + find: + paths: "{{ role_path }}/tests/common" + patterns: "{{ testcase }}.yaml" + connection: local + register: test_cases + +- name: collect nxapi test cases + find: + paths: "{{ role_path }}/tests/nxapi" + patterns: "{{ testcase }}.yaml" + connection: local + register: nxapi_cases + +- set_fact: + test_cases: + files: "{{ test_cases.files }} + {{ nxapi_cases.files }}" + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=httpapi) + include: "{{ test_case_to_run }} ansible_connection=httpapi connection={{ nxapi }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/nxos_bfd_interfaces/tests/cli/deleted.yaml b/test/integration/targets/nxos_bfd_interfaces/tests/cli/deleted.yaml new file mode 100644 index 00000000000..1db9a217f91 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/tests/cli/deleted.yaml @@ -0,0 +1,71 @@ +--- +- debug: + msg: "Start nxos_bfd_interfaces deleted integration tests connection={{ ansible_connection }}" + +- set_fact: test_int1="{{ nxos_int1 }}" +- set_fact: + bfd_enable: enable + bfd_disable: disable + when: platform is not search('N5K|N6K') + +- name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + default interface {{ test_int1 }} + +- block: + - name: setup2 + cli_config: + config: | + feature bfd + interface {{ test_int1 }} + no switchport + + - name: setup initial bfd state + nxos_bfd_interfaces: + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + echo: disable + state: merged + + - name: Gather bfd_interfaces facts + nxos_facts: &facts + gather_subset: + - '!all' + - '!min' + gather_network_resources: bfd_interfaces + + - name: deleted + nxos_bfd_interfaces: &deleted + config: + - name: "{{ test_int1 }}" + state: deleted + register: result + + - assert: + that: + - "result.changed == true" + - "'bfd echo' in result.commands" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - assert: + that: + - "{{ 'bfd' in result.commands }}" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + when: bfd_enable is defined + + + - name: Idempotence - deleted + nxos_bfd_interfaces: *deleted + register: result + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + + always: + - name: teardown + cli_config: *setup_teardown diff --git a/test/integration/targets/nxos_bfd_interfaces/tests/cli/merged.yaml b/test/integration/targets/nxos_bfd_interfaces/tests/cli/merged.yaml new file mode 100644 index 00000000000..730a9498f44 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/tests/cli/merged.yaml @@ -0,0 +1,68 @@ +--- +- debug: + msg: "Start nxos_bfd_interfaces merged integration tests connection={{ ansible_connection }}" + +- set_fact: test_int1="{{ nxos_int1 }}" +- set_fact: + bfd_enable: enable + bfd_disable: disable + when: platform is not search('N5K|N6K') + +- name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + default interface {{ test_int1 }} + +- block: + - name: setup2 + cli_config: + config: | + feature bfd + interface {{ test_int1 }} + no switchport + + - name: Merged + nxos_bfd_interfaces: &merged + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + echo: disable + state: merged + register: result + + - assert: + that: + - "result.changed == true" + - "'no bfd echo' in result.commands" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - assert: + that: + - "{{ 'no bfd' in result.commands }}" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + when: bfd_enable is defined + + - name: Gather bfd_interfaces facts + nxos_facts: + gather_subset: + - '!all' + - '!min' + gather_network_resources: bfd_interfaces + + - assert: + that: + - "ansible_facts.network_resources.bfd_interfaces|symmetric_difference(result.after)|length == 0" + + - name: Idempotence - Merged + nxos_bfd_interfaces: *merged + register: result + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + + always: + - name: teardown + cli_config: *setup_teardown diff --git a/test/integration/targets/nxos_bfd_interfaces/tests/cli/overridden.yaml b/test/integration/targets/nxos_bfd_interfaces/tests/cli/overridden.yaml new file mode 100644 index 00000000000..c4f5e95b1c7 --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/tests/cli/overridden.yaml @@ -0,0 +1,67 @@ +--- +- debug: + msg: "Start nxos_bfd_interfaces overridden integration tests connection={{ ansible_connection }}" + +- set_fact: test_int1="{{ nxos_int1 }}" +- set_fact: test_int2="{{ nxos_int2 }}" +- set_fact: + bfd_enable: enable + bfd_disable: disable + when: platform is not search('N5K|N6K') + +- name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + default interface {{ test_int1 }} + default interface {{ test_int2 }} + +- block: + - name: setup2 + cli_config: + config: | + feature bfd + interface {{ test_int1 }} + no switchport + interface {{ test_int2 }} + no switchport + + - name: setup initial bfd state + nxos_bfd_interfaces: + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + echo: enable + - name: "{{ test_int2 }}" + bfd: "{{ bfd_enable|default(omit)}}" + echo: disable + state: merged + + - name: Overridden + nxos_bfd_interfaces: &overridden + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + echo: disable + state: overridden + register: result + + - assert: + that: + - result.changed == true + - result.commands[1] == 'bfd echo' # test_int2 reset to defaults + - result.commands[3] == 'no bfd echo' # test_int1 set to playval + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - name: Idempotence - Overridden + nxos_bfd_interfaces: *overridden + register: result + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + + always: + - name: teardown + cli_config: *setup_teardown diff --git a/test/integration/targets/nxos_bfd_interfaces/tests/cli/replaced.yaml b/test/integration/targets/nxos_bfd_interfaces/tests/cli/replaced.yaml new file mode 100644 index 00000000000..fc48c2d3f7a --- /dev/null +++ b/test/integration/targets/nxos_bfd_interfaces/tests/cli/replaced.yaml @@ -0,0 +1,65 @@ +--- +- debug: + msg: "Start nxos_bfd_interfaces replaced integration tests connection={{ ansible_connection }}" + +- set_fact: test_int1="{{ nxos_int1 }}" +- set_fact: + bfd_enable: enable + bfd_disable: disable + when: platform is not search('N5K|N6K') + +- name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + default interface {{ test_int1 }} + +- block: + - name: setup2 + cli_config: + config: | + feature bfd + interface {{ test_int1 }} + no switchport + + - name: setup initial bfd state + nxos_bfd_interfaces: + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + echo: enable + state: merged + + - name: Replaced + nxos_bfd_interfaces: &replaced + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_enable|default(omit)}}" + echo: disable + state: replaced + register: result + + - assert: + that: + - "result.changed == true" + - "'no bfd echo' in result.commands" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - assert: + that: + - "{{ 'bfd' in result.commands }}" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + when: bfd_enable is defined + + - name: Idempotence - Replaced + nxos_bfd_interfaces: *replaced + register: result + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + + always: + - name: teardown + cli_config: *setup_teardown diff --git a/test/units/modules/network/nxos/test_nxos_bfd_interfaces.py b/test/units/modules/network/nxos/test_nxos_bfd_interfaces.py new file mode 100644 index 00000000000..83630f29374 --- /dev/null +++ b/test/units/modules/network/nxos/test_nxos_bfd_interfaces.py @@ -0,0 +1,303 @@ +# (c) 2019 Red Hat Inc. +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from textwrap import dedent +from units.compat.mock import patch +from units.modules.utils import AnsibleFailJson +from ansible.modules.network.nxos import nxos_bfd_interfaces +from ansible.module_utils.network.nxos.config.bfd_interfaces.bfd_interfaces import Bfd_interfaces +from .nxos_module import TestNxosModule, load_fixture, set_module_args + +ignore_provider_arg = True + + +class TestNxosBfdInterfacesModule(TestNxosModule): + + module = nxos_bfd_interfaces + + def setUp(self): + super(TestNxosBfdInterfacesModule, self).setUp() + + self.mock_FACT_LEGACY_SUBSETS = patch('ansible.module_utils.network.nxos.facts.facts.FACT_LEGACY_SUBSETS') + self.FACT_LEGACY_SUBSETS = self.mock_FACT_LEGACY_SUBSETS.start() + + self.mock_get_resource_connection_config = patch('ansible.module_utils.network.common.cfg.base.get_resource_connection') + self.get_resource_connection_config = self.mock_get_resource_connection_config.start() + + self.mock_get_resource_connection_facts = patch('ansible.module_utils.network.common.facts.facts.get_resource_connection') + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() + + self.mock_edit_config = patch('ansible.module_utils.network.nxos.config.bfd_interfaces.bfd_interfaces.Bfd_interfaces.edit_config') + self.edit_config = self.mock_edit_config.start() + + def tearDown(self): + super(TestNxosBfdInterfacesModule, self).tearDown() + self.mock_FACT_LEGACY_SUBSETS.stop() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_edit_config.stop() + + def load_fixtures(self, commands=None, device=''): + self.mock_FACT_LEGACY_SUBSETS.return_value = dict() + self.get_resource_connection_config.return_value = None + self.edit_config.return_value = None + + # --------------------------- + # Bfd_interfaces Test Cases + # --------------------------- + + # 'state' logic behaviors + # + # - 'merged' : Update existing device state with any differences in the play. + # - 'deleted' : Reset existing device state to default values. Ignores any + # play attrs other than 'name'. Scope is limited to interfaces + # in the play. + # - 'overridden': The play is the source of truth. Similar to replaced but the + # scope includes all interfaces; ie. it will also reset state + # on interfaces not found in the play. + # - 'replaced' : Scope is limited to the interfaces in the play. + + SHOW_CMD = "show running-config | section '^interface|^feature bfd'" + + def test_1(self): + # Setup: No BFD configs shown on device interfaces + # NOTE: The bfd 'enable' state is the default and does not nvgen. + existing = dedent('''\ + feature bfd + interface Ethernet1/1 + interface Ethernet1/2 + interface Ethernet1/3 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict( + name='Ethernet1/1', + bfd='disable', + echo='disable'), + dict( + name='Ethernet1/2', + bfd='disable'), + ]) + # Expected result commands for each 'state' + merged = ['interface Ethernet1/1', 'no bfd', 'no bfd echo', + 'interface Ethernet1/2', 'no bfd'] + deleted = [] + overridden = merged + replaced = merged + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False, commands=deleted) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + + def test_2(self): + # Change existing BFD configs + existing = dedent('''\ + feature bfd + interface Ethernet1/1 + no bfd + interface Ethernet1/2 + no bfd echo + interface Ethernet1/3 + no bfd + no bfd echo + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict( + name='Ethernet1/1', + bfd='enable', + echo='disable'), + dict( + name='Ethernet1/2'), + # Eth1/3 not present! Thus overridden should set Eth1/3 to defaults; + # replaced should ignore Eth1/3. + ]) + # Expected result commands for each 'state' + merged = ['interface Ethernet1/1', 'bfd', 'no bfd echo'] + deleted = ['interface Ethernet1/1', 'bfd', + 'interface Ethernet1/2', 'bfd echo'] + overridden = ['interface Ethernet1/3', 'bfd', 'bfd echo', + 'interface Ethernet1/1', 'bfd', 'no bfd echo', + 'interface Ethernet1/2', 'bfd echo'] + replaced = ['interface Ethernet1/1', 'bfd', 'no bfd echo', + 'interface Ethernet1/2', 'bfd echo'] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + + def test_3(self): + # Device has bfd configs, playbook has no values + existing = dedent('''\ + feature bfd + interface Ethernet1/1 + no bfd + interface Ethernet1/2 + no bfd echo + interface Ethernet1/3 + no bfd + no bfd echo + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/1'), + ]) + # Expected result commands for each 'state' + merged = [] + deleted = ['interface Ethernet1/1', 'bfd'] + overridden = ['interface Ethernet1/1', 'bfd', + 'interface Ethernet1/2', 'bfd echo', + 'interface Ethernet1/3', 'bfd', 'bfd echo'] + replaced = ['interface Ethernet1/1', 'bfd'] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + + def test_4(self): + # Test with interface that doesn't exist yet + existing = dedent('''\ + feature bfd + interface Ethernet1/1 + no bfd + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict( + name='Ethernet1/1.42', + bfd='enable', + echo='disable'), + ]) + # Expected result commands for each 'state' + merged = ['interface Ethernet1/1.42', 'bfd', 'no bfd echo'] + deleted = [] + overridden = ['interface Ethernet1/1.42', 'bfd', 'no bfd echo', + 'interface Ethernet1/1', 'bfd'] + replaced = ['interface Ethernet1/1.42', 'bfd', 'no bfd echo'] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False, commands=deleted) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=overridden) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=replaced) + + def test_5(self): + # idempotence + existing = dedent('''\ + feature bfd + interface Ethernet1/1 + no bfd + no bfd echo + interface Ethernet1/2 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict( + name='Ethernet1/1', + bfd='disable', + echo='disable'), + dict( + name='Ethernet1/2', + bfd='enable', + echo='enable'), + ]) + # Expected result commands for each 'state' + merged = [] + deleted = ['interface Ethernet1/1', 'bfd', 'bfd echo'] + overridden = [] + replaced = [] + + playbook['state'] = 'merged' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False, commands=merged) + + playbook['state'] = 'deleted' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=True, commands=deleted) + + playbook['state'] = 'overridden' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False, commands=overridden) + + playbook['state'] = 'replaced' + set_module_args(playbook, ignore_provider_arg) + self.execute_module(changed=False, commands=replaced) + + +def build_args(data, type, state=None, check_mode=None): + if state is None: + state = 'merged' + if check_mode is None: + check_mode = False + args = { + 'state': state, + '_ansible_check_mode': check_mode, + 'config': { + type: data + } + } + return args