diff --git a/lib/ansible/modules/network/eos/eos_interface.py b/lib/ansible/modules/network/eos/eos_interface.py new file mode 100644 index 0000000000..5eb1c5df36 --- /dev/null +++ b/lib/ansible/modules/network/eos/eos_interface.py @@ -0,0 +1,464 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Ansible by Red Hat, inc +# 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'], + 'supported_by': 'network'} + + +DOCUMENTATION = """ +--- +module: eos_interface +version_added: "2.5" +author: "Ganesh Nalawade (@ganeshrn)" +short_description: Manage Interface on Arista EOS network devices +description: + - This module provides declarative management of Interfaces + on Arista EOS network devices. +notes: + - Tested against EOS 4.15 +options: + name: + description: + - Name of the Interface to be configured on remote device. + required: true + description: + description: + - Description of Interface upto 240 characters. + enabled: + description: + - Interface link status. If the value is I(True) the interface state will be + enabled, else if value is I(False) interface will be in disable (shutdown) state. + default: True + speed: + description: + - This option configures autoneg and speed/duplex/flowcontrol for the interface + given in C(name) option. + mtu: + description: + - Set maximum transmission unit size in bytes of transmit packet for the interface given + in C(name) option. + tx_rate: + description: + - Transmit rate in bits per second (bps) for the interface given in C(name) option. + rx_rate: + description: + - Receiver rate in bits per second (bps) for the interface given in C(name) option. + neighbors: + description: + - Check the operational state of given interface C(name) for LLDP neighbor. + - The following suboptions are available. + suboptions: + host: + description: + - "LLDP neighbor host for given interface C(name)." + port: + description: + - "LLDP neighbor port to which given interface C(name) is connected." + aggregate: + description: + - List of Interfaces definitions. Each of the entry in aggregate list should + define name of interface C(name) and other options as required. + 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 + 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 + choices: ['present', 'absent', 'up', 'down'] +""" + +EXAMPLES = """ +- name: configure interface + eos_interface: + name: ethernet1 + description: test-interface + speed: 100full + mtu: 512 + +- name: remove interface + eos_interface: + name: ethernet1 + state: absent + +- name: make interface up + eos_interface: + name: ethernet1 + enabled: True + +- name: make interface down + eos_interface: + name: ethernet1 + enabled: False + +- name: Check intent arguments + eos_interface: + name: ethernet1 + state: up + tx_rate: ge(0) + rx_rate: le(0) + +- name: Check neighbors intent arguments + eos_interface: + name: ethernet1 + neighbors: + - port: eth0 + host: netdev + +- name: Configure interface in disabled state and check if the operational state is disabled or not + eos_interface: + name: ethernet1 + enabled: False + state: down + +- name: Add interface using aggregate + eos_interface: + aggregate: + - { name: ethernet1, mtu: 256, description: test-interface-1 } + - { name: ethernet2, mtu: 516, description: test-interface-2 } + speed: 100full + state: present + +- name: Delete interface using aggregate + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: absent +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device. + returned: always, except for the platforms that use Netconf transport to manage the device. + type: list + sample: + - interface ethernet1 + - description test-interface + - speed 100full + - mtu 512 +""" +import re +from copy import deepcopy +from time import sleep + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.config import NetworkConfig +from ansible.module_utils.network.common.utils import conditional, remove_default_spec +from ansible.module_utils.network.eos.eos import get_config, load_config, run_commands +from ansible.module_utils.network.eos.eos import eos_argument_spec + + +def validate_mtu(value, module): + if value and not 68 <= int(value) <= 65535: + module.fail_json(msg='mtu must be between 68 and 65535') + + +def validate_param_values(module, obj, param=None): + if param is None: + param = module.params + for key in obj: + # validate the param value (if validator func exists) + validator = globals().get('validate_%s' % key) + if callable(validator): + validator(param.get(key), module) + + +def parse_shutdown(configobj, name): + cfg = configobj['interface %s' % name] + cfg = '\n'.join(cfg.children) + match = re.search(r'shutdown', cfg, re.M) + return True if match else False + + +def parse_config_argument(configobj, name, arg=None): + cfg = configobj['interface %s' % name] + cfg = '\n'.join(cfg.children) + match = re.search(r'%s (.+)$' % arg, cfg, re.M) + if match: + return match.group(1) + + +def search_obj_in_list(name, lst): + for o in lst: + if o['name'] == name: + return o + + return None + + +def add_command_to_interface(interface, cmd, commands): + if interface not in commands: + commands.append(interface) + commands.append(cmd) + + +def map_config_to_obj(module): + config = get_config(module) + configobj = NetworkConfig(indent=3, contents=config) + + match = re.findall(r'^interface (\S+)', config, re.M) + if not match: + return list() + + instances = list() + + for item in set(match): + obj = { + 'name': item.lower(), + 'description': parse_config_argument(configobj, item, 'description'), + 'speed': parse_config_argument(configobj, item, 'speed'), + 'mtu': parse_config_argument(configobj, item, 'mtu'), + 'disable': parse_shutdown(configobj, item), + 'state': 'present' + } + instances.append(obj) + return instances + + +def map_params_to_obj(module): + obj = [] + aggregate = module.params.get('aggregate') + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + item['name'] = item['name'].lower() + validate_param_values(module, item, item) + d = item.copy() + + if d['enabled']: + d['disable'] = False + else: + d['disable'] = True + + obj.append(d) + + else: + params = { + 'name': module.params['name'].lower(), + 'description': module.params['description'], + 'speed': module.params['speed'], + 'mtu': module.params['mtu'], + 'state': module.params['state'], + 'delay': module.params['delay'], + 'tx_rate': module.params['tx_rate'], + 'rx_rate': module.params['rx_rate'], + 'neighbors': module.params['neighbors'] + } + + validate_param_values(module, params) + if module.params['enabled']: + params.update({'disable': False}) + else: + params.update({'disable': True}) + + obj.append(params) + return obj + + +def map_obj_to_commands(updates, modules): + commands = list() + want, have = updates + + args = ('speed', 'description', 'mtu') + for w in want: + name = w['name'] + disable = w['disable'] + state = w['state'] + + obj_in_have = search_obj_in_list(name, have) + interface = 'interface ' + name + + if state == 'absent' and obj_in_have: + commands.append('no ' + interface) + + elif state in ('present', 'up', 'down'): + if obj_in_have: + for item in args: + candidate = w.get(item) + running = obj_in_have.get(item) + if candidate != running: + if candidate: + cmd = "{0} {1}".format(item, candidate) + add_command_to_interface(interface, cmd, commands) + + if disable and not obj_in_have.get('disable', False): + add_command_to_interface(interface, 'shutdown', commands) + elif not disable and obj_in_have.get('disable', False): + add_command_to_interface(interface, 'no shutdown', commands) + else: + commands.append(interface) + for item in args: + value = w.get(item) + if value: + commands.append("{0} {1}".format(item, value)) + + if disable: + commands.append('no shutdown') + return commands + + +def check_declarative_intent_params(module, want, result): + failed_conditions = [] + have_neighbors = None + for w in want: + want_state = w.get('state') + want_tx_rate = w.get('tx_rate') + want_rx_rate = w.get('rx_rate') + want_neighbors = w.get('neighbors') + + if want_state not in ('up', 'down') and not want_tx_rate and not want_rx_rate and not want_neighbors: + continue + + if result['changed']: + sleep(w['delay']) + + command = 'show interfaces %s' % w['name'] + output = run_commands(module, [command]) + + if want_state in ('up', 'down'): + match = re.search(r'%s (\w+)' % 'line protocol is', output[0], re.M) + have_state = None + if match: + have_state = match.group(1) + if have_state is None or not conditional(want_state, have_state.strip()): + failed_conditions.append('state ' + 'eq(%s)' % want_state) + + if want_tx_rate: + match = re.search(r'%s (\d+)' % 'output rate', output[0], re.M) + have_tx_rate = None + if match: + have_tx_rate = match.group(1) + + if have_tx_rate is None or not conditional(want_tx_rate, have_tx_rate.strip(), cast=int): + failed_conditions.append('tx_rate ' + want_tx_rate) + + if want_rx_rate: + match = re.search(r'%s (\d+)' % 'input rate', output[0], re.M) + have_rx_rate = None + if match: + have_rx_rate = match.group(1) + + if have_rx_rate is None or not conditional(want_rx_rate, have_rx_rate.strip(), cast=int): + failed_conditions.append('rx_rate ' + want_rx_rate) + + if want_neighbors: + have_host = [] + have_port = [] + if have_neighbors is None: + have_neighbors = run_commands(module, ['show lldp neighbors {}'.format(w['name'])]) + + if have_neighbors[0]: + lines = have_neighbors[0].strip().split('\n') + col = None + for index, line in enumerate(lines): + if re.search(r"^Port\s+Neighbor Device ID\s+Neighbor Port ID\s+TTL", line): + col = index + break + + if col and col < len(lines) - 1: + for items in lines[col + 1:]: + value = re.split(r'\s+', items) + try: + have_port.append(value[2]) + have_host.append(value[1]) + except IndexError: + pass + + for item in want_neighbors: + host = item.get('host') + port = item.get('port') + if host and host not in have_host: + failed_conditions.append('host ' + host) + if port and port not in have_port: + failed_conditions.append('port ' + port) + return failed_conditions + + +def main(): + """ main entry point for module execution + """ + neighbors_spec = dict( + host=dict(), + port=dict() + ) + + element_spec = dict( + name=dict(), + description=dict(), + speed=dict(), + mtu=dict(), + enabled=dict(default=True, type='bool'), + tx_rate=dict(), + rx_rate=dict(), + neighbors=dict(type='list', elements='dict', options=neighbors_spec), + delay=dict(default=10, type='int'), + state=dict(default='present', + choices=['present', 'absent', 'up', 'down']) + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec['name'] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type='list', elements='dict', options=aggregate_spec), + ) + + argument_spec.update(element_spec) + argument_spec.update(eos_argument_spec) + + required_one_of = [['name', 'aggregate']] + mutually_exclusive = [['name', 'aggregate']] + + module = AnsibleModule(argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + warnings = list() + result = {'changed': False} + if warnings: + result['warnings'] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + commands = map_obj_to_commands((want, have), module) + result['commands'] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + if response.get('diff') and module._diff: + result['diff'] = {'prepared': response.get('diff')} + result['session_name'] = response.get('session') + result['changed'] = True + + failed_conditions = check_declarative_intent_params(module, want, result) + + if failed_conditions: + msg = 'One or more conditional statements have not been satisfied' + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/network/eos/eos_l3_interface.py b/lib/ansible/modules/network/eos/eos_l3_interface.py index 21b8c9ac97..68cfcb9073 100644 --- a/lib/ansible/modules/network/eos/eos_l3_interface.py +++ b/lib/ansible/modules/network/eos/eos_l3_interface.py @@ -220,6 +220,7 @@ def map_params_to_obj(module): if item.get(key) is None: item[key] = module.params[key] + item['name'] = item['name'].lower() validate_param_values(module, item, item) obj.append(item.copy()) else: diff --git a/test/integration/eos.yaml b/test/integration/eos.yaml index 3790b3cc9c..4ea390719e 100644 --- a/test/integration/eos.yaml +++ b/test/integration/eos.yaml @@ -15,6 +15,7 @@ - set_fact: test_failed: false failed_modules: [] + - block: - include_role: name: eos_banner @@ -130,6 +131,14 @@ failed_modules: "{{ failed_modules }} + [ 'eos_l3_interface' ]" test_failed: true + - block: + - include_role: + name: eos_interface + when: "limit_to in ['*', 'eos_interface']" + rescue: + - set_fact: + failed_modules: "{{ failed_modules }} + [ 'eos_interface' ]" + test_failed: true ########### - debug: var=failed_modules when: test_failed diff --git a/test/integration/targets/eos_interface/defaults/main.yaml b/test/integration/targets/eos_interface/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/eos_interface/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/eos_interface/meta/main.yml b/test/integration/targets/eos_interface/meta/main.yml new file mode 100644 index 0000000000..e5c8cd02f0 --- /dev/null +++ b/test/integration/targets/eos_interface/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_eos_tests diff --git a/test/integration/targets/eos_interface/tasks/cli.yaml b/test/integration/targets/eos_interface/tasks/cli.yaml new file mode 100644 index 0000000000..a6f7ae0351 --- /dev/null +++ b/test/integration/targets/eos_interface/tasks/cli.yaml @@ -0,0 +1,22 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + register: test_cases + delegate_to: localhost + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + +- name: run test case (connection=local) + include: "{{ test_case_to_run }} ansible_connection=local ansible_become=no" + with_first_found: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/eos_interface/tasks/eapi.yaml b/test/integration/targets/eos_interface/tasks/eapi.yaml new file mode 100644 index 0000000000..bda1df677a --- /dev/null +++ b/test/integration/targets/eos_interface/tasks/eapi.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all eapi test cases + find: + paths: "{{ role_path }}/tests/eapi" + 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 case (connection=local) + include: "{{ test_case_to_run }} ansible_connection=local" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/eos_interface/tasks/main.yaml b/test/integration/targets/eos_interface/tasks/main.yaml new file mode 100644 index 0000000000..415c99d8b1 --- /dev/null +++ b/test/integration/targets/eos_interface/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: cli.yaml, tags: ['cli'] } diff --git a/test/integration/targets/eos_interface/tests/cli/basic.yaml b/test/integration/targets/eos_interface/tests/cli/basic.yaml new file mode 100644 index 0000000000..c713a9b3af --- /dev/null +++ b/test/integration/targets/eos_interface/tests/cli/basic.yaml @@ -0,0 +1,245 @@ +--- +- debug: msg="START eos_interface cli/basic.yaml on connection={{ ansible_connection }}" + +- name: Set test interface + set_fact: + test_interface_1: ethernet1 + test_interface_2: ethernet2 + +- name: Configure interface (setup) + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-1 + mtu: 1800 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- name: Configure interface + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-initial + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"description test-interface-initial" in result.commands' + +- name: Confgure interface (idempotent) + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-initial + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Confgure interface parameters + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface + mtu: 2000 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"description test-interface" in result.commands' + - '"mtu 2000" in result.commands' + +- name: Change interface parameters + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-1 + mtu: 1800 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"description test-interface-1" in result.commands' + - '"mtu 1800" in result.commands' + +- name: Disable interface + eos_interface: + name: "{{ test_interface_1 }}" + enabled: False + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"shutdown" in result.commands' + +- name: Enable interface + eos_interface: + name: "{{ test_interface_1 }}" + enabled: True + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"no shutdown" in result.commands' + +- name: Confgure second interface (setup) + eos_interface: + name: "{{ test_interface_2 }}" + description: test-interface-initial + mtu: 1800 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- name: Add interface aggregate + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}", mtu: 2000, description: test-interface-1 } + - { name: "{{ test_interface_2 }}", mtu: 2000, description: test-interface-2 } + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"mtu 2000" in result.commands' + - '"interface {{ test_interface_2 }}" in result.commands' + - '"description test-interface-2" in result.commands' + - '"mtu 2000" in result.commands' + +- name: Add interface aggregate (idempotent) + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}", mtu: 2000, description: test-interface-1 } + - { name: "{{ test_interface_2 }}", mtu: 2000, description: test-interface-2 } + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Disable interface aggregate + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}" } + - { name: "{{ test_interface_2 }}" } + enabled: False + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"shutdown" in result.commands' + - '"interface {{ test_interface_2 }}" in result.commands' + - '"shutdown" in result.commands' + +- name: Enable interface aggregate + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}" } + - { name: "{{ test_interface_2 }}" } + enabled: True + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"no shutdown" in result.commands' + - '"interface {{ test_interface_2 }}" in result.commands' + - '"no shutdown" in result.commands' + +- name: loopback interface setup + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: absent + authorize: yes + provider: "{{ cli }}" + +- name: Create loopback interface aggregate + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: present + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface loopback9" in result.commands' + - '"interface loopback10" in result.commands' + +- name: Delete loopback interface aggregate + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"no interface loopback9" in result.commands' + - '"no interface loopback10" in result.commands' + +- name: Delete loopback interface aggregate (idempotent) + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: absent + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- debug: msg="END eos_interface cli/basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/eos_interface/tests/cli/intent.yaml b/test/integration/targets/eos_interface/tests/cli/intent.yaml new file mode 100644 index 0000000000..033996f044 --- /dev/null +++ b/test/integration/targets/eos_interface/tests/cli/intent.yaml @@ -0,0 +1,161 @@ +--- +- debug: msg="START eos_interface cli/intent.yaml on connection={{ ansible_connection }}" + +- name: Set test interface + set_fact: + test_interface_1: ethernet1 + test_interface_2: ethernet2 + +- name: Check intent arguments + eos_interface: + name: "{{ test_interface_1 }}" + state: up + tx_rate: ge(0) + rx_rate: ge(0) + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - "result.failed == false" + +- name: Check intent arguments (failed condition) + eos_interface: + name: "{{ test_interface_1 }}" + state: down + tx_rate: gt(0) + rx_rate: lt(0) + authorize: yes + provider: "{{ cli }}" + ignore_errors: yes + register: result + +- assert: + that: + - "result.failed == true" + - "'state eq(down)' in result.failed_conditions" + - "'tx_rate gt(0)' in result.failed_conditions" + - "'rx_rate lt(0)' in result.failed_conditions" + +- name: Config + intent + eos_interface: + name: "{{ test_interface_1 }}" + enabled: False + state: down + authorize: yes + provider: "{{ cli }}" + register: result + +- assert: + that: + - "result.failed == false" + +- name: Config + intent (fail) + eos_interface: + name: "{{ test_interface_1 }}" + enabled: False + authorize: yes + state: up + provider: "{{ cli }}" + ignore_errors: yes + register: result + +- assert: + that: + - "result.failed == true" + - "'state eq(up)' in result.failed_conditions" + +- name: Register show neighbors detail + eos_command: + commands: + - show lldp neighbors management1 + authorize: yes + provider: "{{ cli }}" + register: show_lldp_neighbors_result + +- block: + - name: Check neighbors intent arguments + eos_interface: + name: management1 + neighbors: + - port: eth0 + host: an-vyos-02 + authorize: yes + provider: "{{ cli }}" + register: result + + - assert: + that: + - "result.failed == false" + + - name: Check neighbors intent arguments (failed condition) + eos_interface: + name: management1 + neighbors: + - port: dummy_port + host: dummy_host + authorize: yes + provider: "{{ cli }}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.failed == true" + - "'host dummy_host' in result.failed_conditions" + - "'port dummy_port' in result.failed_conditions" + when: '"an-vyos-02" in show_lldp_neighbors_result.stdout[0]' + +- name: Aggregate config + intent (pass) + eos_interface: + aggregate: + - name: "{{ test_interface_1 }}" + enabled: True + state: up + authorize: yes + provider: "{{ cli }}" + ignore_errors: yes + register: result + +- assert: + that: + - "result.failed == false" + +- block: + - name: Aggregate neighbors intent (pass) + eos_interface: + aggregate: + - name: management1 + neighbors: + - port: eth0 + host: an-vyos-02 + authorize: yes + provider: "{{ cli }}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.failed == false" + + - name: Aggregate neighbors intent (fail) + eos_interface: + aggregate: + - name: management1 + neighbors: + - port: eth0 + host: an-vyos-02 + - port: dummy_port + host: dummy_host + authorize: yes + provider: "{{ cli }}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.failed == true" + - "'host dummy_host' in result.failed_conditions" + - "'port dummy_port' in result.failed_conditions" + when: "'an-vyos-02' in show_lldp_neighbors_result.stdout[0]" diff --git a/test/integration/targets/eos_interface/tests/eapi/basic.yaml b/test/integration/targets/eos_interface/tests/eapi/basic.yaml new file mode 100644 index 0000000000..bddfeac941 --- /dev/null +++ b/test/integration/targets/eos_interface/tests/eapi/basic.yaml @@ -0,0 +1,245 @@ +--- +- debug: msg="START eos_interface eapi/basic.yaml on connection={{ ansible_connection }}" + +- name: Set test interface + set_fact: + test_interface_1: ethernet1 + test_interface_2: ethernet2 + +- name: Configure interface (setup) + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-1 + mtu: 1800 + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- name: Configure interface + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-initial + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"description test-interface-initial" in result.commands' + +- name: Confgure interface (idempotent) + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-initial + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Confgure interface parameters + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface + mtu: 2000 + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"description test-interface" in result.commands' + - '"mtu 2000" in result.commands' + +- name: Change interface parameters + eos_interface: + name: "{{ test_interface_1 }}" + description: test-interface-1 + mtu: 1800 + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"description test-interface-1" in result.commands' + - '"mtu 1800" in result.commands' + +- name: Disable interface + eos_interface: + name: "{{ test_interface_1 }}" + enabled: False + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"shutdown" in result.commands' + +- name: Enable interface + eos_interface: + name: "{{ test_interface_1 }}" + enabled: True + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"no shutdown" in result.commands' + +- name: Confgure second interface (setup) + eos_interface: + name: "{{ test_interface_2 }}" + description: test-interface-initial + mtu: 1800 + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- name: Add interface aggregate + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}", mtu: 2000, description: test-interface-1 } + - { name: "{{ test_interface_2 }}", mtu: 2000, description: test-interface-2 } + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"mtu 2000" in result.commands' + - '"interface {{ test_interface_2 }}" in result.commands' + - '"description test-interface-2" in result.commands' + - '"mtu 2000" in result.commands' + +- name: Add interface aggregate (idempotent) + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}", mtu: 2000, description: test-interface-1 } + - { name: "{{ test_interface_2 }}", mtu: 2000, description: test-interface-2 } + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- name: Disable interface aggregate + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}" } + - { name: "{{ test_interface_2 }}" } + enabled: False + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"shutdown" in result.commands' + - '"interface {{ test_interface_2 }}" in result.commands' + - '"shutdown" in result.commands' + +- name: Enable interface aggregate + eos_interface: + aggregate: + - { name: "{{ test_interface_1 }}" } + - { name: "{{ test_interface_2 }}" } + enabled: True + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface {{ test_interface_1 }}" in result.commands' + - '"no shutdown" in result.commands' + - '"interface {{ test_interface_2 }}" in result.commands' + - '"no shutdown" in result.commands' + +- name: loopback interface setup + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: absent + authorize: yes + provider: "{{ eapi }}" + +- name: Create loopback interface aggregate + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: present + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"interface loopback9" in result.commands' + - '"interface loopback10" in result.commands' + +- name: Delete loopback interface aggregate + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: absent + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == true' + - '"no interface loopback9" in result.commands' + - '"no interface loopback10" in result.commands' + +- name: Delete loopback interface aggregate (idempotent) + eos_interface: + aggregate: + - name: loopback9 + - name: loopback10 + state: absent + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - 'result.changed == false' + +- debug: msg="END eos_interface eapi/basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/eos_interface/tests/eapi/intent.yaml b/test/integration/targets/eos_interface/tests/eapi/intent.yaml new file mode 100644 index 0000000000..fd01c9f498 --- /dev/null +++ b/test/integration/targets/eos_interface/tests/eapi/intent.yaml @@ -0,0 +1,161 @@ +--- +- debug: msg="START eos_interface eapi/intent.yaml on connection={{ ansible_connection }}" + +- name: Set test interface + set_fact: + test_interface_1: ethernet1 + test_interface_2: ethernet2 + +- name: Check intent arguments + eos_interface: + name: "{{ test_interface_1 }}" + state: up + tx_rate: ge(0) + rx_rate: ge(0) + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - "result.failed == false" + +- name: Check intent arguments (failed condition) + eos_interface: + name: "{{ test_interface_1 }}" + state: down + tx_rate: gt(0) + rx_rate: lt(0) + authorize: yes + provider: "{{ eapi }}" + ignore_errors: yes + register: result + +- assert: + that: + - "result.failed == true" + - "'state eq(down)' in result.failed_conditions" + - "'tx_rate gt(0)' in result.failed_conditions" + - "'rx_rate lt(0)' in result.failed_conditions" + +- name: Config + intent + eos_interface: + name: "{{ test_interface_1 }}" + enabled: False + state: down + authorize: yes + provider: "{{ eapi }}" + register: result + +- assert: + that: + - "result.failed == false" + +- name: Config + intent (fail) + eos_interface: + name: "{{ test_interface_1 }}" + enabled: False + authorize: yes + state: up + provider: "{{ eapi }}" + ignore_errors: yes + register: result + +- assert: + that: + - "result.failed == true" + - "'state eq(up)' in result.failed_conditions" + +- name: Register show neighbors detail + eos_command: + commands: + - show lldp neighbors management1 + authorize: yes + provider: "{{ eapi }}" + register: show_lldp_neighbors_result + +- block: + - name: Check neighbors intent arguments + eos_interface: + name: management1 + neighbors: + - port: eth0 + host: an-vyos-02 + authorize: yes + provider: "{{ eapi }}" + register: result + + - assert: + that: + - "result.failed == false" + + - name: Check neighbors intent arguments (failed condition) + eos_interface: + name: management1 + neighbors: + - port: dummy_port + host: dummy_host + authorize: yes + provider: "{{ eapi }}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.failed == true" + - "'host dummy_host' in result.failed_conditions" + - "'port dummy_port' in result.failed_conditions" + when: '"an-vyos-02" in show_lldp_neighbors_result.stdout[0]' + +- name: Aggregate config + intent (pass) + eos_interface: + aggregate: + - name: "{{ test_interface_1 }}" + enabled: True + state: up + authorize: yes + provider: "{{ eapi }}" + ignore_errors: yes + register: result + +- assert: + that: + - "result.failed == false" + +- block: + - name: Aggregate neighbors intent (pass) + eos_interface: + aggregate: + - name: management1 + neighbors: + - port: eth0 + host: an-vyos-02 + authorize: yes + provider: "{{ eapi }}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.failed == false" + + - name: Aggregate neighbors intent (fail) + eos_interface: + aggregate: + - name: management1 + neighbors: + - port: eth0 + host: an-vyos-02 + - port: dummy_port + host: dummy_host + authorize: yes + provider: "{{ eapi }}" + ignore_errors: yes + register: result + + - assert: + that: + - "result.failed == true" + - "'host dummy_host' in result.failed_conditions" + - "'port dummy_port' in result.failed_conditions" + when: "'an-vyos-02' in show_lldp_neighbors_result.stdout[0]" diff --git a/test/integration/targets/ios_interface/tests/cli/intent.yaml b/test/integration/targets/ios_interface/tests/cli/intent.yaml index 7dec4cf44b..ec3a58c24e 100644 --- a/test/integration/targets/ios_interface/tests/cli/intent.yaml +++ b/test/integration/targets/ios_interface/tests/cli/intent.yaml @@ -108,7 +108,7 @@ - "result.failed == true" - "'host dummy_host' in result.failed_conditions" - "'port dummy_port' in result.failed_conditions" - when: '"netdev" in show_lldp_neighbors_result.stdout' + when: '"netdev" in show_lldp_neighbors_result.stdout[0]' - name: Aggregate config + intent (pass) ios_interface: @@ -158,4 +158,4 @@ - "result.failed == true" - "'host dummy_host' in result.failed_conditions" - "'port dummy_port' in result.failed_conditions" - when: "'netdev' in show_lldp_neighbors_result.stdout" + when: "'netdev' in show_lldp_neighbors_result.stdout[0]"