diff --git a/lib/ansible/module_utils/network/nxos/argspec/hsrp_interfaces/__init__.py b/lib/ansible/module_utils/network/nxos/argspec/hsrp_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/argspec/hsrp_interfaces/hsrp_interfaces.py b/lib/ansible/module_utils/network/nxos/argspec/hsrp_interfaces/hsrp_interfaces.py new file mode 100644 index 00000000000..5183ce7ac8b --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/argspec/hsrp_interfaces/hsrp_interfaces.py @@ -0,0 +1,54 @@ +# +# -*- 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_hsrp_interfaces module +""" + + +class Hsrp_interfacesArgs(object): # pylint: disable=R0903 + """The arg spec for the nxos_hsrp_interfaces module + """ + + def __init__(self, **kwargs): + pass + + argument_spec = { + 'config': { + 'type': 'list', + 'elements': 'dict', + 'options': { + 'name': {'type': 'str'}, + 'bfd': { + 'choices': ['enable', 'disable'], 'type': 'str'}, + }, + }, + 'state': { + 'choices': ['merged', 'replaced', 'overridden', 'deleted'], + 'default': 'merged', + 'type': 'str' + } + } # pylint: disable=C0301 diff --git a/lib/ansible/module_utils/network/nxos/config/hsrp_interfaces/__init__.py b/lib/ansible/module_utils/network/nxos/config/hsrp_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/config/hsrp_interfaces/hsrp_interfaces.py b/lib/ansible/module_utils/network/nxos/config/hsrp_interfaces/hsrp_interfaces.py new file mode 100644 index 00000000000..e3ef78e540c --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/config/hsrp_interfaces/hsrp_interfaces.py @@ -0,0 +1,248 @@ +# +# -*- 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) +""" +The nxos hsrp_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 + +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 Hsrp_interfaces(ConfigBase): + """ + The nxos_hsrp_interfaces class + """ + + gather_subset = [ + '!all', + '!min', + ] + + gather_network_resources = [ + 'hsrp_interfaces', + ] + + def __init__(self, module): + super(Hsrp_interfaces, self).__init__(module) + + def get_hsrp_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) + hsrp_interfaces_facts = facts['ansible_network_resources'].get('hsrp_interfaces', []) + return hsrp_interfaces_facts + + 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_hsrp_interfaces_facts = self.get_hsrp_interfaces_facts() + cmds.extend(self.set_config(existing_hsrp_interfaces_facts)) + if cmds: + if not self._module.check_mode: + self.edit_config(cmds) + result['changed'] = True + result['commands'] = cmds + changed_hsrp_interfaces_facts = self.get_hsrp_interfaces_facts() + + result['before'] = existing_hsrp_interfaces_facts + if result['changed']: + result['after'] = changed_hsrp_interfaces_facts + + result['warnings'] = warnings + return result + + def set_config(self, existing_hsrp_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 + """ + config = self._module.params['config'] + want = [] + if config: + for w in config: + w.update({'name': normalize_interface(w['name'])}) + want.append(w) + have = existing_hsrp_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'] + # check for 'config' keyword in play + 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: + # Check existing states, set to default if not in want or different than want + h = flatten_dict(h) + obj_in_want = 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 = [] + if 'bfd' in obj: + cmds.append('no hsrp bfd') + 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'] = 'disable' + 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, obj_in_have): + if not want: + return [] + cmds = [] + if 'bfd' in want and want['bfd'] is not None: + if want['bfd'] == 'enable': + cmd = 'hsrp bfd' + cmds.append(cmd) + elif want['bfd'] == 'disable' and obj_in_have and obj_in_have.get('bfd') == 'enable': + cmd = 'no hsrp bfd' + cmds.append(cmd) + + if cmds: + cmds.insert(0, 'interface ' + want['name']) + return cmds + + def set_commands(self, want, have): + cmds = [] + obj_in_have = search_obj_in_list(want['name'], have, 'name') + if not obj_in_have: + cmds = self.add_commands(want, obj_in_have) + else: + diff = self.diff_of_dicts(want, obj_in_have) + cmds = self.add_commands(diff, obj_in_have) + return cmds diff --git a/lib/ansible/module_utils/network/nxos/facts/facts.py b/lib/ansible/module_utils/network/nxos/facts/facts.py index 24825837af1..84d6fd32778 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.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.hsrp_interfaces.hsrp_interfaces import Hsrp_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 @@ -33,6 +34,7 @@ FACT_LEGACY_SUBSETS = dict( ) FACT_RESOURCE_SUBSETS = dict( bfd_interfaces=Bfd_interfacesFacts, + hsrp_interfaces=Hsrp_interfacesFacts, lag_interfaces=Lag_interfacesFacts, lldp_global=Lldp_globalFacts, telemetry=TelemetryFacts, diff --git a/lib/ansible/module_utils/network/nxos/facts/hsrp_interfaces/__init__.py b/lib/ansible/module_utils/network/nxos/facts/hsrp_interfaces/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/module_utils/network/nxos/facts/hsrp_interfaces/hsrp_interfaces.py b/lib/ansible/module_utils/network/nxos/facts/hsrp_interfaces/hsrp_interfaces.py new file mode 100644 index 00000000000..b63eca4afdd --- /dev/null +++ b/lib/ansible/module_utils/network/nxos/facts/hsrp_interfaces/hsrp_interfaces.py @@ -0,0 +1,89 @@ +# +# -*- 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 hsrp_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.hsrp_interfaces.hsrp_interfaces import Hsrp_interfacesArgs +from ansible.module_utils.network.nxos.utils.utils import get_interface_type + + +class Hsrp_interfacesFacts(object): + """ The nxos hsrp_interfaces fact class + """ + + def __init__(self, module, subspec='config', options='options'): + self._module = module + self.argument_spec = Hsrp_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 hsrp_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') + + resources = data.split('interface ') + 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('hsrp_interfaces', None) + facts = {} + if objs: + facts['hsrp_interfaces'] = [] + params = utils.validate_config(self.argument_spec, {'config': objs}) + for cfg in params['config']: + facts['hsrp_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 + config['bfd'] = utils.parse_conf_cmd_arg(conf, 'hsrp bfd', 'enable', 'disable') + + return utils.remove_empties(config) diff --git a/lib/ansible/modules/network/nxos/nxos_hsrp_interfaces.py b/lib/ansible/modules/network/nxos/nxos_hsrp_interfaces.py new file mode 100644 index 00000000000..e6750eba1be --- /dev/null +++ b/lib/ansible/modules/network/nxos/nxos_hsrp_interfaces.py @@ -0,0 +1,163 @@ +#!/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_hsrp_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_hsrp_interfaces +version_added: "2.10" +short_description: 'Manages HSRP attributes of NXOS interfaces.' +description: 'Manages Hot Standby Router Protocol (HSRP) interface attributes.' +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 HSRP Bidirectional Forwarding Detection (BFD) 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 hsrp attributes on interfaces + nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + - name: Ethernet1/2 + operation: deleted + + +# Using merged + +- name: Configure hsrp attributes on interfaces + nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + - name: Ethernet1/2 + bfd: disable + operation: merged + + +# Using overridden + +- name: Configure hsrp attributes on interfaces + nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + - name: Ethernet1/2 + bfd: disable + operation: overridden + + +# Using replaced + +- name: Configure hsrp attributes on interfaces + nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + - name: Ethernet1/2 + bfd: 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', 'hsrp bfd'] +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.nxos.argspec.hsrp_interfaces.hsrp_interfaces import Hsrp_interfacesArgs +from ansible.module_utils.network.nxos.config.hsrp_interfaces.hsrp_interfaces import Hsrp_interfaces + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Hsrp_interfacesArgs.argument_spec, + supports_check_mode=True) + + result = Hsrp_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/nxos_hsrp_interfaces/defaults/main.yaml b/test/integration/targets/nxos_hsrp_interfaces/defaults/main.yaml new file mode 100644 index 00000000000..5f709c5aac1 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_interfaces/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/nxos_hsrp_interfaces/meta/main.yml b/test/integration/targets/nxos_hsrp_interfaces/meta/main.yml new file mode 100644 index 00000000000..ae741cbdc71 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_interfaces/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_nxos_tests diff --git a/test/integration/targets/nxos_hsrp_interfaces/tasks/cli.yaml b/test/integration/targets/nxos_hsrp_interfaces/tasks/cli.yaml new file mode 100644 index 00000000000..6c7ea4a7f94 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_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_hsrp_interfaces/tasks/main.yaml b/test/integration/targets/nxos_hsrp_interfaces/tasks/main.yaml new file mode 100644 index 00000000000..415c99d8b12 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_interfaces/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/nxos_hsrp_interfaces/tasks/nxapi.yaml b/test/integration/targets/nxos_hsrp_interfaces/tasks/nxapi.yaml new file mode 100644 index 00000000000..cbf41b92947 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_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_hsrp_interfaces/tests/cli/deleted.yaml b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/deleted.yaml new file mode 100644 index 00000000000..01331dfd2d3 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/deleted.yaml @@ -0,0 +1,57 @@ +--- +- debug: + msg: "Start nxos_hsrp_interfaces deleted 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('N35') + +- block: + - name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + no feature hsrp + default interface {{ test_int1 }} + default interface {{ test_int2 }} + + - name: setup2 + cli_config: + config: | + feature bfd + feature hsrp + interface {{ test_int1 }} + no switchport + hsrp bfd + interface {{ test_int2 }} + no switchport + + - name: deleted + nxos_hsrp_interfaces: &deleted + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + state: deleted + register: result + + - assert: + that: + - "result.changed == true" + - "'no hsrp bfd' in result.commands" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - name: Idempotence - deleted + nxos_hsrp_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_hsrp_interfaces/tests/cli/merged.yaml b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/merged.yaml new file mode 100644 index 00000000000..baf56458fc9 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/merged.yaml @@ -0,0 +1,65 @@ +--- +- debug: + msg: "Start nxos_hsrp_interfaces merged 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('N35') + +- block: + - name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + no feature hsrp + default interface {{ test_int1 }} + default interface {{ test_int2 }} + + - name: setup2 + cli_config: + config: | + feature bfd + feature hsrp + interface {{ test_int1 }} + no switchport + hsrp bfd + interface {{ test_int2 }} + no switchport + + - name: Merged + nxos_hsrp_interfaces: &merged + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + state: merged + register: result + + - assert: + that: + - "result.changed == true" + - "'no hsrp bfd' in result.commands" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - name: Gather hsrp_interfaces facts + nxos_facts: + gather_subset: + - '!all' + - '!min' + gather_network_resources: hsrp_interfaces + + - name: Idempotence - Merged + nxos_hsrp_interfaces: *merged + register: result + + - assert: + that: + - "result.changed == false" + - "result.commands|length == 0" + when: bfd_enable is defined + + always: + - name: teardown + cli_config: *setup_teardown diff --git a/test/integration/targets/nxos_hsrp_interfaces/tests/cli/overridden.yaml b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/overridden.yaml new file mode 100644 index 00000000000..5d2aa441393 --- /dev/null +++ b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/overridden.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: "Start nxos_hsrp_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('N35') + +- block: + - name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + no feature hsrp + default interface {{ test_int1 }} + default interface {{ test_int2 }} + + - name: setup2 + cli_config: + config: | + feature bfd + feature hsrp + interface {{ test_int1 }} + no switchport + interface {{ test_int2 }} + no switchport + hsrp bfd + + - name: Overridden + nxos_hsrp_interfaces: &overridden + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_enable|default(omit)}}" + state: overridden + register: result + + - assert: + that: + - result.changed == true + - result.commands[1] == 'no hsrp bfd' # test_int2 reset to defaults + - result.commands[3] == 'hsrp bfd' # test_int1 set to playval + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - name: Idempotence - Overridden + nxos_hsrp_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_hsrp_interfaces/tests/cli/replaced.yaml b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/replaced.yaml new file mode 100644 index 00000000000..6eed615368d --- /dev/null +++ b/test/integration/targets/nxos_hsrp_interfaces/tests/cli/replaced.yaml @@ -0,0 +1,57 @@ +--- +- debug: + msg: "Start nxos_hsrp_interfaces replaced 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('N35') + +- block: + - name: setup1 + cli_config: &setup_teardown + config: | + no feature bfd + no feature hsrp + default interface {{ test_int1 }} + default interface {{ test_int2 }} + + - name: setup2 + cli_config: + config: | + feature bfd + feature hsrp + interface {{ test_int1 }} + no switchport + hsrp bfd + interface {{ test_int2 }} + no switchport + + - name: Replaced + nxos_hsrp_interfaces: &replaced + config: + - name: "{{ test_int1 }}" + bfd: "{{ bfd_disable|default(omit)}}" + state: replaced + register: result + + - assert: + that: + - "result.changed == true" + - "'no hsrp bfd' in result.commands" + msg: "Assert failed. 'result.commands': {{ result.commands }}" + + - name: Idempotence - Replaced + nxos_hsrp_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_hsrp_interfaces.py b/test/units/modules/network/nxos/test_nxos_hsrp_interfaces.py new file mode 100644 index 00000000000..91225520f22 --- /dev/null +++ b/test/units/modules/network/nxos/test_nxos_hsrp_interfaces.py @@ -0,0 +1,293 @@ +# (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_hsrp_interfaces +from ansible.module_utils.network.nxos.config.hsrp_interfaces.hsrp_interfaces import Hsrp_interfaces +from .nxos_module import TestNxosModule, load_fixture, set_module_args + +ignore_provider_arg = True + + +class TestNxosHsrpInterfacesModule(TestNxosModule): + + module = nxos_hsrp_interfaces + + def setUp(self): + super(TestNxosHsrpInterfacesModule, 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.hsrp_interfaces.hsrp_interfaces.Hsrp_interfaces.edit_config') + self.edit_config = self.mock_edit_config.start() + + def tearDown(self): + super(TestNxosHsrpInterfacesModule, 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 + + # --------------------------- + # Hsrp_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' + + def test_1(self): + # Setup: No HSRP BFD configs shown on device interfaces + existing = dedent('''\ + 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='enable'), + dict( + name='Ethernet1/2', + bfd='disable'), + ]) + # Expected result commands for each 'state' + merged = ['interface Ethernet1/1', 'hsrp 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 HSRP configs + existing = dedent('''\ + interface Ethernet1/1 + hsrp bfd + interface Ethernet1/2 + hsrp bfd + interface Ethernet1/3 + hsrp bfd + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict( + name='Ethernet1/1', + bfd='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', 'no hsrp bfd'] + deleted = ['interface Ethernet1/1', 'no hsrp bfd', + 'interface Ethernet1/2', 'no hsrp bfd'] + overridden = ['interface Ethernet1/3', 'no hsrp bfd', + 'interface Ethernet1/1', 'no hsrp bfd', + 'interface Ethernet1/2', 'no hsrp bfd'] + replaced = ['interface Ethernet1/1', 'no hsrp bfd', + 'interface Ethernet1/2', 'no hsrp bfd'] + + 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 hsrp bfd configs, playbook has no values + existing = dedent('''\ + interface Ethernet1/1 + hsrp bfd + interface Ethernet1/2 + hsrp bfd + interface Ethernet1/3 + hsrp bfd + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict(name='Ethernet1/1'), + dict(name='Ethernet1/2'), + ]) + # Expected result commands for each 'state' + merged = [] + deleted = ['interface Ethernet1/1', 'no hsrp bfd', + 'interface Ethernet1/2', 'no hsrp bfd'] + overridden = ['interface Ethernet1/3', 'no hsrp bfd', + 'interface Ethernet1/1', 'no hsrp bfd', + 'interface Ethernet1/2', 'no hsrp bfd'] + replaced = ['interface Ethernet1/1', 'no hsrp bfd', + 'interface Ethernet1/2', 'no hsrp 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('''\ + interface Ethernet1/1 + hsrp bfd + interface Ethernet1/2 + hsrp bfd + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict( + name='Ethernet1/1.42', + bfd='enable'), + ]) + # Expected result commands for each 'state' + merged = ['interface Ethernet1/1.42', 'hsrp bfd'] + deleted = [] + overridden = ['interface Ethernet1/1.42', 'hsrp bfd', + 'interface Ethernet1/1', 'no hsrp bfd', + 'interface Ethernet1/2', 'no hsrp bfd'] + replaced = ['interface Ethernet1/1.42', 'hsrp bfd'] + + 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('''\ + interface Ethernet1/1 + hsrp bfd + interface Ethernet1/2 + ''') + self.get_resource_connection_facts.return_value = {self.SHOW_CMD: existing} + playbook = dict(config=[ + dict( + name='Ethernet1/1', + bfd='enable'), + dict( + name='Ethernet1/2', + bfd='disable'), + ]) + # Expected result commands for each 'state' + merged = [] + deleted = ['interface Ethernet1/1', 'no hsrp bfd'] + 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