diff --git a/lib/ansible/modules/network/cli/cli_config.py b/lib/ansible/modules/network/cli/cli_config.py new file mode 100644 index 00000000000..fdbedf7442c --- /dev/null +++ b/lib/ansible/modules/network/cli/cli_config.py @@ -0,0 +1,326 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, 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: cli_config +version_added: "2.7" +author: "Trishna Guha (@trishnaguha)" +short_description: Push text based configuration to network devices over network_cli +description: + - This module provides platform agnostic way of pushing text based + configuration to network devices over network_cli connection plugin. +options: + config: + description: + - The config to be pushed to the network device. This is a + required argument. + required: true + type: 'str' + commit: + description: + - The C(commit) argument instructs the module to push the + configuration to the device. This is mapped to module check mode. + type: 'bool' + replace: + description: + - If the C(replace) argument is set to C(yes), it will replace + the entire running-config of the device with the C(config) + argument value. For NXOS devices, C(replace) argument takes + path to the file on the device that will be used for replacing + the entire running-config. Nexus 9K devices only support replace. + Use I(net_put) or I(nxos_file_copy) module to copy the flat file + to remote device and then use set the fullpath to this argument. + type: 'str' + rollback: + description: + - The C(rollback) argument instructs the module to rollback the + current configuration to the identifier specified in the + argument. If the specified rollback identifier does not + exist on the remote device, the module will fail. To rollback + to the most recent commit, set the C(rollback) argument to 0. + commit_comment: + description: + - The C(commit_comment) argument specifies a text string to be used + when committing the configuration. If the C(commit) argument + is set to False, this argument is silently ignored. This argument + is only valid for the platforms that support commit operation + with comment. + type: 'str' + defaults: + description: + - The I(defaults) argument will influence how the running-config + is collected from the device. When the value is set to true, + the command used to collect the running-config is append with + the all keyword. When the value is set to false, the command + is issued without the all keyword. + default: 'no' + type: 'bool' + multiline_delimiter: + description: + - This argument is used when pushing a multiline configuration + element to the device. It specifies the character to use as + the delimiting character. This only applies to the configuration + action. + type: 'str' + diff_replace: + description: + - Instructs the module on the way to perform the configuration + on the device. If the C(diff_replace) argument is set to I(line) + then the modified lines are pushed to the device in configuration + mode. If the argument is set to I(block) then the entire command + block is pushed to the device in configuration mode if any + line is not correct. Note that this parameter will be ignored if + the platform has onbox diff support. + choices: ['line', 'block', 'config'] + diff_match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device config. If C(diff_match) + is set to I(line), commands are matched line by line. If C(diff_match) + is set to I(strict), command lines are matched with respect to position. + If C(diff_match) is set to I(exact), command lines must be an equal match. + Finally, if C(diff_match) is set to I(none), the module will not attempt + to compare the source configuration with the running configuration on the + remote device. Note that this parameter will be ignored if the platform + has onbox diff support. + choices: ['line', 'strict', 'exact', 'none'] + diff_ignore_lines: + description: + - Use this argument to specify one or more lines that should be + ignored during the diff. This is used for lines in the configuration + that are automatically updated by the system. This argument takes + a list of regular expressions or exact line matches. + Note that this parameter will be ignored if the platform has onbox + diff support. +""" + +EXAMPLES = """ +- name: configure device with config + cli_config: + config: "{{ lookup('template', 'basic/config.j2') }}" + +- name: configure device with config with defaults enabled + cli_config: + config: "{{ lookup('template', 'basic/config.j2') }}" + defaults: yes + +- name: Use diff_match + cli_config: + config: | + interface loopback999 + no description + shutdown + diff_match: none + +- name: nxos replace config + cli_config: + replace: 'bootflash:nxoscfg' + +- name: commit with comment + cli_config: + config: set system host-name foo + commit_comment: this is a test +""" + +RETURN = """ +commands: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['interface Loopback999', 'no shutdown'] +""" + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection +from ansible.module_utils._text import to_text + + +def validate_args(module, capabilities): + """validate param if it is supported on the platform + """ + if (module.params['replace'] and + not capabilities['device_operations']['supports_replace']): + module.fail_json(msg='replace is not supported on this platform') + + if (module.params['rollback'] and + not capabilities['device_operations']['supports_rollback']): + module.fail_json(msg='rollback is not supported on this platform') + + if (module.params['commit_comment'] and + not capabilities['device_operations']['supports_commit_comment']): + module.fail_json(msg='commit_comment is not supported on this platform') + + if (module.params['defaults'] and + not capabilities['device_operations']['supports_defaults']): + module.fail_json(msg='defaults is not supported on this platform') + + if (module.params['multiline_delimiter'] and + not capabilities['device_operations']['supports_multiline_delimiter']): + module.fail_json(msg='multiline_delimiter is not supported on this platform') + + if (module.params['diff_replace'] and + not capabilities['device_operations']['supports_diff_replace']): + module.fail_json(msg='diff_replace is not supported on this platform') + + if (module.params['diff_match'] and + not capabilities['device_operations']['supports_diff_match']): + module.fail_json(msg='diff_match is not supported on this platform') + + if (module.params['diff_ignore_lines'] and + not capabilities['device_operations']['supports_diff_ignore_lines']): + module.fail_json(msg='diff_ignore_lines is not supported on this platform') + + +def run(module, capabilities, connection, candidate, running): + result = {} + resp = {} + config_diff = [] + banner_diff = {} + + replace = module.params['replace'] + rollback = module.params['rollback'] + commit_comment = module.params['commit_comment'] + multiline_delimiter = module.params['multiline_delimiter'] + diff_replace = module.params['diff_replace'] + diff_match = module.params['diff_match'] + diff_ignore_lines = module.params['diff_ignore_lines'] + + commit = not module.check_mode + + if replace in ('yes', 'true', 'True'): + replace = True + elif replace in ('no', 'false', 'False'): + replace = False + + if capabilities['device_operations']['supports_generate_diff']: + kwargs = {'candidate': candidate, 'running': running} + if diff_match: + kwargs.update({'diff_match': diff_match}) + if diff_replace: + kwargs.update({'diff_replace': diff_replace}) + if diff_ignore_lines: + kwargs.update({'diff_ignore_lines': diff_ignore_lines}) + + diff_response = connection.get_diff(**kwargs) + + config_diff = diff_response.get('config_diff') + banner_diff = diff_response.get('banner_diff') + + if config_diff: + if isinstance(config_diff, list): + candidate = config_diff + else: + candidate = config_diff.splitlines() + + kwargs = {'candidate': candidate, 'commit': commit, 'replace': replace, + 'comment': commit_comment} + connection.edit_config(**kwargs) + result['changed'] = True + + if banner_diff: + candidate = json.dumps(banner_diff) + + kwargs = {'candidate': candidate, 'commit': commit} + if multiline_delimiter: + kwargs.update({'multiline_delimiter': multiline_delimiter}) + connection.edit_banner(**kwargs) + result['changed'] = True + + elif capabilities['device_operations']['supports_onbox_diff']: + if diff_replace: + module.warn('diff_replace is ignored as the device supports onbox diff') + if diff_match: + module.warn('diff_mattch is ignored as the device supports onbox diff') + if diff_ignore_lines: + module.warn('diff_ignore_lines is ignored as the device supports onbox diff') + + if not isinstance(candidate, list): + candidate = candidate.strip('\n').splitlines() + + kwargs = {'candidate': candidate, 'commit': commit, 'replace': replace, + 'comment': commit_comment} + resp = connection.edit_config(**kwargs) + + if 'diff' in resp: + result['changed'] = True + + if module._diff: + if 'diff' in resp: + result['diff'] = {'prepared': resp['diff']} + else: + diff = '' + if config_diff: + if isinstance(config_diff, list): + diff += '\n'.join(config_diff) + else: + diff += config_diff + if banner_diff: + diff += json.dumps(banner_diff) + result['diff'] = {'prepared': diff} + + return result + + +def main(): + """main entry point for execution + """ + argument_spec = dict( + config=dict(required=True, type='str'), + commit=dict(type='bool'), + replace=dict(type='str'), + rollback=dict(type='int'), + commit_comment=dict(type='str'), + defaults=dict(default=False, type='bool'), + multiline_delimiter=dict(type='str'), + diff_replace=dict(choices=['line', 'block', 'config']), + diff_match=dict(choices=['line', 'strict', 'exact', 'none']), + diff_ignore_lines=dict(type='list') + ) + + module = AnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + result = {'changed': False} + + connection = Connection(module._socket_path) + capabilities = module.from_json(connection.get_capabilities()) + + if capabilities: + validate_args(module, capabilities) + + if module.params['defaults']: + if 'get_default_flag' in capabilities.get('rpc'): + flags = connection.get_default_flag() + else: + flags = 'all' + else: + flags = [] + + candidate = to_text(module.params['config']) + running = connection.get_config(flags=flags) + + try: + result.update(run(module, capabilities, connection, candidate, running)) + except Exception as exc: + module.fail_json(msg=to_text(exc)) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/plugins/action/cli_config.py b/lib/ansible/plugins/action/cli_config.py new file mode 100644 index 00000000000..3041b10049a --- /dev/null +++ b/lib/ansible/plugins/action/cli_config.py @@ -0,0 +1,31 @@ +# +# Copyright 2018 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 . +# +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.plugins.action.normal import ActionModule as _ActionModule + + +class ActionModule(_ActionModule): + + def run(self, tmp=None, task_vars=None): + if self._play_context.connection != 'network_cli': + return {'failed': True, 'msg': 'Connection type %s is not valid for cli_config module' % self._play_context.connection} + + return super(ActionModule, self).run(task_vars=task_vars) diff --git a/lib/ansible/plugins/cliconf/vyos.py b/lib/ansible/plugins/cliconf/vyos.py index 01381066b0d..895ae0f61a8 100644 --- a/lib/ansible/plugins/cliconf/vyos.py +++ b/lib/ansible/plugins/cliconf/vyos.py @@ -99,7 +99,8 @@ class Cliconf(CliconfBase): else: self.send_command('exit') - resp['diff'] = diff_config + if diff_config: + resp['diff'] = diff_config resp['response'] = results resp['request'] = requests return resp diff --git a/test/integration/targets/eos_config/tasks/cli_config.yaml b/test/integration/targets/eos_config/tasks/cli_config.yaml new file mode 100644 index 00000000000..f5d5b551fb8 --- /dev/null +++ b/test/integration/targets/eos_config/tasks/cli_config.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli_config test cases + find: + paths: "{{ role_path }}/tests/cli_config" + 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 case (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/eos_config/tasks/main.yaml b/test/integration/targets/eos_config/tasks/main.yaml index 970e74171ea..8befd2711f5 100644 --- a/test/integration/targets/eos_config/tasks/main.yaml +++ b/test/integration/targets/eos_config/tasks/main.yaml @@ -1,3 +1,4 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: cli_config.yaml, tags: ['cli_config'] } - { include: eapi.yaml, tags: ['eapi'] } diff --git a/test/integration/targets/eos_config/tests/cli_config/cli_basic.yaml b/test/integration/targets/eos_config/tests/cli_config/cli_basic.yaml new file mode 100644 index 00000000000..d7419cd6b04 --- /dev/null +++ b/test/integration/targets/eos_config/tests/cli_config/cli_basic.yaml @@ -0,0 +1,33 @@ +--- +- debug: msg="START cli_config/cli_basic.yaml on connection={{ ansible_connection }}" + +- name: setup + cli_config: &rm + config: | + interface Ethernet2 + no description + no shutdown + become: yes + +- name: configure device with config + cli_config: &conf + config: "{{ lookup('template', 'basic/config.j2') }}" + register: result + become: yes + +- assert: + that: + - "result.changed == true" + +- name: Idempotence + cli_config: *conf + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ios_config/tasks/cli_config.yaml b/test/integration/targets/ios_config/tasks/cli_config.yaml new file mode 100644 index 00000000000..f5d5b551fb8 --- /dev/null +++ b/test/integration/targets/ios_config/tasks/cli_config.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli_config test cases + find: + paths: "{{ role_path }}/tests/cli_config" + 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 case (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/ios_config/tasks/main.yaml b/test/integration/targets/ios_config/tasks/main.yaml index 415c99d8b12..5e327e8d521 100644 --- a/test/integration/targets/ios_config/tasks/main.yaml +++ b/test/integration/targets/ios_config/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: cli_config.yaml, tags: ['cli_config'] } diff --git a/test/integration/targets/ios_config/templates/basic/configblock.j2 b/test/integration/targets/ios_config/templates/basic/configblock.j2 new file mode 100644 index 00000000000..46150c4d900 --- /dev/null +++ b/test/integration/targets/ios_config/templates/basic/configblock.j2 @@ -0,0 +1,5 @@ +ip access-list extended test + permit ip host 192.0.2.1 any log + permit ip host 192.0.2.2 any log + permit ip host 192.0.2.3 any log + permit ip host 192.0.2.4 any log diff --git a/test/integration/targets/ios_config/templates/basic/configexact1.j2 b/test/integration/targets/ios_config/templates/basic/configexact1.j2 new file mode 100644 index 00000000000..82ad080c5d9 --- /dev/null +++ b/test/integration/targets/ios_config/templates/basic/configexact1.j2 @@ -0,0 +1,6 @@ +no ip access-list extended test +ip access-list extended test + permit ip host 192.0.2.1 any log + permit ip host 192.0.2.2 any log + permit ip host 192.0.2.3 any log + permit ip host 192.0.2.4 any log diff --git a/test/integration/targets/ios_config/templates/basic/configexact2.j2 b/test/integration/targets/ios_config/templates/basic/configexact2.j2 new file mode 100644 index 00000000000..46150c4d900 --- /dev/null +++ b/test/integration/targets/ios_config/templates/basic/configexact2.j2 @@ -0,0 +1,5 @@ +ip access-list extended test + permit ip host 192.0.2.1 any log + permit ip host 192.0.2.2 any log + permit ip host 192.0.2.3 any log + permit ip host 192.0.2.4 any log diff --git a/test/integration/targets/ios_config/templates/basic/configstrict1.j2 b/test/integration/targets/ios_config/templates/basic/configstrict1.j2 new file mode 100644 index 00000000000..46150c4d900 --- /dev/null +++ b/test/integration/targets/ios_config/templates/basic/configstrict1.j2 @@ -0,0 +1,5 @@ +ip access-list extended test + permit ip host 192.0.2.1 any log + permit ip host 192.0.2.2 any log + permit ip host 192.0.2.3 any log + permit ip host 192.0.2.4 any log diff --git a/test/integration/targets/ios_config/templates/basic/setupblock.j2 b/test/integration/targets/ios_config/templates/basic/setupblock.j2 new file mode 100644 index 00000000000..f57aa49b732 --- /dev/null +++ b/test/integration/targets/ios_config/templates/basic/setupblock.j2 @@ -0,0 +1,5 @@ +no ip access-list extended test +ip access-list extended test + permit ip host 192.0.2.1 any log + permit ip host 192.0.2.2 any log + permit ip host 192.0.2.3 any log diff --git a/test/integration/targets/ios_config/templates/basic/setupexact.j2 b/test/integration/targets/ios_config/templates/basic/setupexact.j2 new file mode 100644 index 00000000000..e8791e272e1 --- /dev/null +++ b/test/integration/targets/ios_config/templates/basic/setupexact.j2 @@ -0,0 +1,7 @@ +no ip access-list extended test +ip access-list extended test + permit ip host 192.0.2.1 any log + permit ip host 192.0.2.2 any log + permit ip host 192.0.2.3 any log + permit ip host 192.0.2.4 any log + permit ip host 192.0.2.5 any log diff --git a/test/integration/targets/ios_config/templates/basic/setupstrict.j2 b/test/integration/targets/ios_config/templates/basic/setupstrict.j2 new file mode 100644 index 00000000000..e8791e272e1 --- /dev/null +++ b/test/integration/targets/ios_config/templates/basic/setupstrict.j2 @@ -0,0 +1,7 @@ +no ip access-list extended test +ip access-list extended test + permit ip host 192.0.2.1 any log + permit ip host 192.0.2.2 any log + permit ip host 192.0.2.3 any log + permit ip host 192.0.2.4 any log + permit ip host 192.0.2.5 any log diff --git a/test/integration/targets/ios_config/tests/cli_config/cli_basic.yaml b/test/integration/targets/ios_config/tests/cli_config/cli_basic.yaml new file mode 100644 index 00000000000..607b3df498b --- /dev/null +++ b/test/integration/targets/ios_config/tests/cli_config/cli_basic.yaml @@ -0,0 +1,45 @@ +--- +- debug: msg="START cli_config/cli_basic.yaml on connection={{ ansible_connection }}" + +- name: setup + cli_config: &rm + config: | + interface loopback999 + no description + shutdown + diff_match: none + +- name: configure device with config + cli_config: &conf + config: "{{ lookup('template', 'basic/config.j2') }}" + register: result + +- assert: + that: + - "result.changed == true" + +- name: Idempotence + cli_config: *conf + register: result + +- assert: + that: + - "result.changed == false" + +- name: remove config + cli_config: *rm + +- name: configure device with config + cli_config: + config: "{{ lookup('template', 'basic/config.j2') }}" + defaults: yes + register: result + +- assert: + that: + - "result.changed == true" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ios_config/tests/cli_config/cli_block_replace.yaml b/test/integration/targets/ios_config/tests/cli_config/cli_block_replace.yaml new file mode 100644 index 00000000000..f66f83f96db --- /dev/null +++ b/test/integration/targets/ios_config/tests/cli_config/cli_block_replace.yaml @@ -0,0 +1,32 @@ +--- +- debug: msg="START cli_config/cli_block_replace.yaml on connection={{ ansible_connection }}" + +- name: setup - remove configuration + cli_config: + config: "{{ lookup('template', 'basic/setupblock.j2') }}" + diff_match: none + +- name: block replace + cli_config: &block + config: "{{ lookup('template', 'basic/configblock.j2') }}" + diff_replace: block + register: result + +- assert: + that: + - "result.changed == true" + +- name: block replace (Idempotence) + cli_config: *block + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: + config: no ip access-list extended test + diff_match: none + +- debug: msg="END cli_config/cli_block_replace.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ios_config/tests/cli_config/cli_exact_match.yaml b/test/integration/targets/ios_config/tests/cli_config/cli_exact_match.yaml new file mode 100644 index 00000000000..597afa50994 --- /dev/null +++ b/test/integration/targets/ios_config/tests/cli_config/cli_exact_match.yaml @@ -0,0 +1,33 @@ +--- +- debug: msg="START cli_config/cli_exact_match.yaml on connection={{ ansible_connection }}" + +- name: setup - remove configuration + cli_config: + config: "{{ lookup('template', 'basic/setupexact.j2') }}" + diff_match: none + +- name: configure using exact match + cli_config: + config: "{{ lookup('template', 'basic/configexact1.j2') }}" + diff_match: exact + register: result + +- assert: + that: + - "result.changed == true" + +- name: check using exact match + cli_config: + config: "{{ lookup('template', 'basic/configexact2.j2') }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: + config: no ip access-list extended test + diff_match: none + +- debug: msg="END cli_config/cli_exact_match.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/ios_config/tests/cli_config/cli_strict_match.yaml b/test/integration/targets/ios_config/tests/cli_config/cli_strict_match.yaml new file mode 100644 index 00000000000..45ba54f1ef3 --- /dev/null +++ b/test/integration/targets/ios_config/tests/cli_config/cli_strict_match.yaml @@ -0,0 +1,24 @@ +--- +- debug: msg="START cli_config/cli_strict_match.yaml on connection={{ ansible_connection }}" + +- name: setup - remove configuration + cli_config: + config: "{{ lookup('template', 'basic/setupstrict.j2') }}" + diff_match: none + +- name: configure using strict match + cli_config: + config: "{{ lookup('template', 'basic/configstrict1.j2') }}" + diff_match: strict + register: result + +- assert: + that: + - "result.changed == true" + +- name: teardown + cli_config: + config: no ip access-list extended test + diff_match: none + +- debug: msg="END cli_config/cli_strict_match.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/iosxr_config/tasks/cli_config.yaml b/test/integration/targets/iosxr_config/tasks/cli_config.yaml new file mode 100644 index 00000000000..a12f077120d --- /dev/null +++ b/test/integration/targets/iosxr_config/tasks/cli_config.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli_config" + 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 case (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/iosxr_config/tasks/main.yaml b/test/integration/targets/iosxr_config/tasks/main.yaml index 415c99d8b12..5e327e8d521 100644 --- a/test/integration/targets/iosxr_config/tasks/main.yaml +++ b/test/integration/targets/iosxr_config/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: cli_config.yaml, tags: ['cli_config'] } diff --git a/test/integration/targets/iosxr_config/tests/cli_config/cli_basic.yaml b/test/integration/targets/iosxr_config/tests/cli_config/cli_basic.yaml new file mode 100644 index 00000000000..b742483fb37 --- /dev/null +++ b/test/integration/targets/iosxr_config/tests/cli_config/cli_basic.yaml @@ -0,0 +1,33 @@ +--- +- debug: msg="START cli_config/cli_basic.yaml on connection={{ ansible_connection }}" + +- name: setup + cli_config: &rm + config: | + interface Loopback999 + no description + no shutdown + become: yes + +- name: configure device with config + cli_config: &conf + config: "{{ lookup('template', 'basic/config.j2') }}" + register: result + become: yes + +- assert: + that: + - "result.changed == true" + +- name: Idempotence + cli_config: *conf + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/junos_config/tasks/cli_config.yaml b/test/integration/targets/junos_config/tasks/cli_config.yaml new file mode 100644 index 00000000000..a12f077120d --- /dev/null +++ b/test/integration/targets/junos_config/tasks/cli_config.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli test cases + find: + paths: "{{ role_path }}/tests/cli_config" + 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 case (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/junos_config/tasks/main.yaml b/test/integration/targets/junos_config/tasks/main.yaml index cc27f174fd8..31f55047141 100644 --- a/test/integration/targets/junos_config/tasks/main.yaml +++ b/test/integration/targets/junos_config/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: netconf.yaml, tags: ['netconf'] } +- { include: cli_config.yaml, tags: ['cli_config'] } diff --git a/test/integration/targets/junos_config/tests/cli_config/cli_basic.yaml b/test/integration/targets/junos_config/tests/cli_config/cli_basic.yaml new file mode 100644 index 00000000000..cf93ddd0e68 --- /dev/null +++ b/test/integration/targets/junos_config/tests/cli_config/cli_basic.yaml @@ -0,0 +1,29 @@ +--- +- debug: msg="START cli_config/cli_basic.yaml on connection={{ ansible_connection }}" + +- name: setup + cli_config: &rm + config: delete interfaces ge-0/0/1 + become: yes + +- name: configure device with config + cli_config: &conf + config: set interfaces ge-0/0/1 description 'test-interface' + register: result + +- assert: + that: + - "result.changed == true" + +- name: Idempotence + cli_config: *conf + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/nxos_config/tasks/cli_config.yaml b/test/integration/targets/nxos_config/tasks/cli_config.yaml new file mode 100644 index 00000000000..f5d5b551fb8 --- /dev/null +++ b/test/integration/targets/nxos_config/tasks/cli_config.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli_config test cases + find: + paths: "{{ role_path }}/tests/cli_config" + 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 case (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/nxos_config/tasks/main.yaml b/test/integration/targets/nxos_config/tasks/main.yaml index 4b0f8c64d90..82ce6f12b29 100644 --- a/test/integration/targets/nxos_config/tasks/main.yaml +++ b/test/integration/targets/nxos_config/tasks/main.yaml @@ -1,3 +1,4 @@ --- - { include: cli.yaml, tags: ['cli'] } - { include: nxapi.yaml, tags: ['nxapi'] } +- { include: cli_config.yaml, tags: ['cli_config'] } diff --git a/test/integration/targets/nxos_config/templates/basic/config.j2 b/test/integration/targets/nxos_config/templates/basic/config.j2 index 9705ee14941..5c79ec8c244 100644 --- a/test/integration/targets/nxos_config/templates/basic/config.j2 +++ b/test/integration/targets/nxos_config/templates/basic/config.j2 @@ -1,4 +1,4 @@ -interface Ethernet2/5 - description this is a test - shutdown +interface loopback1 + description this is a test + shutdown diff --git a/test/integration/targets/nxos_config/templates/basic/configblock.j2 b/test/integration/targets/nxos_config/templates/basic/configblock.j2 new file mode 100644 index 00000000000..ec03c24a22b --- /dev/null +++ b/test/integration/targets/nxos_config/templates/basic/configblock.j2 @@ -0,0 +1,5 @@ +ip access-list test + 10 permit ip 192.0.2.1/32 any log + 20 permit ip 192.0.2.2/32 any log + 30 permit ip 192.0.2.3/32 any log + 40 permit ip 192.0.2.4/32 any log diff --git a/test/integration/targets/nxos_config/templates/basic/configexact1.j2 b/test/integration/targets/nxos_config/templates/basic/configexact1.j2 new file mode 100644 index 00000000000..ec03c24a22b --- /dev/null +++ b/test/integration/targets/nxos_config/templates/basic/configexact1.j2 @@ -0,0 +1,5 @@ +ip access-list test + 10 permit ip 192.0.2.1/32 any log + 20 permit ip 192.0.2.2/32 any log + 30 permit ip 192.0.2.3/32 any log + 40 permit ip 192.0.2.4/32 any log diff --git a/test/integration/targets/nxos_config/templates/basic/configexact2.j2 b/test/integration/targets/nxos_config/templates/basic/configexact2.j2 new file mode 100644 index 00000000000..3fc6800d42a --- /dev/null +++ b/test/integration/targets/nxos_config/templates/basic/configexact2.j2 @@ -0,0 +1,6 @@ +ip access-list test + 10 permit ip 192.0.2.1/32 any log + 20 permit ip 192.0.2.2/32 any log + 30 permit ip 192.0.2.3/32 any log + 40 permit ip 192.0.2.4/32 any log + 50 permit ip 192.0.2.5/32 any log diff --git a/test/integration/targets/nxos_config/templates/basic/configstrict1.j2 b/test/integration/targets/nxos_config/templates/basic/configstrict1.j2 new file mode 100644 index 00000000000..1e7e6f44457 --- /dev/null +++ b/test/integration/targets/nxos_config/templates/basic/configstrict1.j2 @@ -0,0 +1,6 @@ +no ip access-list test +ip access-list test + 10 permit ip 192.0.2.1/32 any log + 20 permit ip 192.0.2.2/32 any log + 30 permit ip 192.0.2.3/32 any log + 40 permit ip 192.0.2.4/32 any log diff --git a/test/integration/targets/nxos_config/templates/basic/setupexact.j2 b/test/integration/targets/nxos_config/templates/basic/setupexact.j2 new file mode 100644 index 00000000000..815e003cc64 --- /dev/null +++ b/test/integration/targets/nxos_config/templates/basic/setupexact.j2 @@ -0,0 +1,7 @@ +no ip access-list test +ip access-list test + 10 permit ip 192.0.2.1/32 any log + 20 permit ip 192.0.2.2/32 any log + 30 permit ip 192.0.2.3/32 any log + 40 permit ip 192.0.2.4/32 any log + 50 permit ip 192.0.2.5/32 any log diff --git a/test/integration/targets/nxos_config/templates/basic/setupstrict.j2 b/test/integration/targets/nxos_config/templates/basic/setupstrict.j2 new file mode 100644 index 00000000000..815e003cc64 --- /dev/null +++ b/test/integration/targets/nxos_config/templates/basic/setupstrict.j2 @@ -0,0 +1,7 @@ +no ip access-list test +ip access-list test + 10 permit ip 192.0.2.1/32 any log + 20 permit ip 192.0.2.2/32 any log + 30 permit ip 192.0.2.3/32 any log + 40 permit ip 192.0.2.4/32 any log + 50 permit ip 192.0.2.5/32 any log diff --git a/test/integration/targets/nxos_config/tests/cli_config/cli_basic.yaml b/test/integration/targets/nxos_config/tests/cli_config/cli_basic.yaml new file mode 100644 index 00000000000..57111467ae8 --- /dev/null +++ b/test/integration/targets/nxos_config/tests/cli_config/cli_basic.yaml @@ -0,0 +1,45 @@ +--- +- debug: msg="START cli_config/cli_basic.yaml on connection={{ ansible_connection }}" + +- name: setup + cli_config: &rm + config: | + interface loopback1 + no description + no shutdown + diff_match: none + +- name: configure device with config + cli_config: &conf + config: "{{ lookup('template', 'basic/config.j2') }}" + register: result + +- assert: + that: + - "result.changed == true" + +- name: Idempotence + cli_config: *conf + register: result + +- assert: + that: + - "result.changed == false" + +- name: remove config + cli_config: *rm + +- name: configure device with config + cli_config: + config: "{{ lookup('template', 'basic/config.j2') }}" + defaults: yes + register: result + +- assert: + that: + - "result.changed == true" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/nxos_config/tests/cli_config/cli_block_replace.yaml b/test/integration/targets/nxos_config/tests/cli_config/cli_block_replace.yaml new file mode 100644 index 00000000000..5cd710048ce --- /dev/null +++ b/test/integration/targets/nxos_config/tests/cli_config/cli_block_replace.yaml @@ -0,0 +1,30 @@ +--- +- debug: msg="START cli_config/cli_block_replace.yaml on connection={{ ansible_connection }}" + +- name: setup - remove configuration + cli_config: &rm + config: "no ip access-list test" + diff_match: none + +- name: block replace + cli_config: &block + config: "{{ lookup('template', 'basic/configblock.j2') }}" + diff_replace: block + register: result + +- assert: + that: + - "result.changed == true" + +- name: block replace (Idempotence) + cli_config: *block + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_block_replace.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/nxos_config/tests/cli_config/cli_exact_match.yaml b/test/integration/targets/nxos_config/tests/cli_config/cli_exact_match.yaml new file mode 100644 index 00000000000..4eb2ee52a9c --- /dev/null +++ b/test/integration/targets/nxos_config/tests/cli_config/cli_exact_match.yaml @@ -0,0 +1,33 @@ +--- +- debug: msg="START cli_config/cli_exact_match.yaml on connection={{ ansible_connection }}" + +- name: setup - remove configuration + cli_config: + config: "{{ lookup('template', 'basic/setupexact.j2') }}" + diff_match: none + +- name: configure using exact match + cli_config: + config: "{{ lookup('template', 'basic/configexact1.j2') }}" + diff_match: exact + register: result + +- assert: + that: + - "result.changed == true" + +- name: check using exact match + cli_config: + config: "{{ lookup('template', 'basic/configexact2.j2') }}" + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: + config: no ip access-list test + diff_match: none + +- debug: msg="END cli_config/cli_exact_match.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/nxos_config/tests/cli_config/cli_strict_match.yaml b/test/integration/targets/nxos_config/tests/cli_config/cli_strict_match.yaml new file mode 100644 index 00000000000..c43b0935005 --- /dev/null +++ b/test/integration/targets/nxos_config/tests/cli_config/cli_strict_match.yaml @@ -0,0 +1,25 @@ +--- +- debug: msg="START cli_config/cli_strict_match.yaml on connection={{ ansible_connection }}" + +- name: setup - remove configuration + cli_config: + config: "{{ lookup('template', 'basic/setupstrict.j2') }}" + diff_match: none + +- name: configure using strict match + cli_config: + config: "{{ lookup('template', 'basic/configstrict1.j2') }}" + diff_match: strict + diff_replace: block + register: result + +- assert: + that: + - "result.changed == true" + +- name: teardown + cli_config: + config: no ip access-list test + diff_match: none + +- debug: msg="END cli_config/cli_strict_match.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/vyos_config/tasks/cli_config.yaml b/test/integration/targets/vyos_config/tasks/cli_config.yaml new file mode 100644 index 00000000000..f5d5b551fb8 --- /dev/null +++ b/test/integration/targets/vyos_config/tasks/cli_config.yaml @@ -0,0 +1,16 @@ +--- +- name: collect all cli_config test cases + find: + paths: "{{ role_path }}/tests/cli_config" + 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 case (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/vyos_config/tasks/main.yaml b/test/integration/targets/vyos_config/tasks/main.yaml index 415c99d8b12..5e327e8d521 100644 --- a/test/integration/targets/vyos_config/tasks/main.yaml +++ b/test/integration/targets/vyos_config/tasks/main.yaml @@ -1,2 +1,3 @@ --- - { include: cli.yaml, tags: ['cli'] } +- { include: cli_config.yaml, tags: ['cli_config'] } diff --git a/test/integration/targets/vyos_config/tests/cli_config/cli_basic.yaml b/test/integration/targets/vyos_config/tests/cli_config/cli_basic.yaml new file mode 100644 index 00000000000..e83db1ea842 --- /dev/null +++ b/test/integration/targets/vyos_config/tests/cli_config/cli_basic.yaml @@ -0,0 +1,28 @@ +--- +- debug: msg="START cli_config/cli_basic.yaml on connection={{ ansible_connection }}" + +- name: setup - remove interface description + cli_config: &rm + config: delete interfaces loopback lo description + +- name: configure device with config + cli_config: &conf + config: set interfaces loopback lo description 'this is a test' + register: result + +- assert: + that: + - "result.changed == true" + +- name: Idempotence + cli_config: *conf + register: result + +- assert: + that: + - "result.changed == false" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_basic.yaml on connection={{ ansible_connection }}" diff --git a/test/integration/targets/vyos_config/tests/cli_config/cli_comment.yaml b/test/integration/targets/vyos_config/tests/cli_config/cli_comment.yaml new file mode 100644 index 00000000000..163f05ee417 --- /dev/null +++ b/test/integration/targets/vyos_config/tests/cli_config/cli_comment.yaml @@ -0,0 +1,30 @@ +--- +- debug: msg="START cli_config/cli_comment.yaml on connection={{ ansible_connection }}" + +- name: setup + cli_config: &rm + config: set system host-name {{ inventory_hostname_short }} + +- name: configure using comment + cli_config: + config: set system host-name foo + commit_comment: this is a test + register: result + +- assert: + that: + - "result.changed == true" + +- name: collect system commits + vyos_command: + commands: show system commit + register: result + +- assert: + that: + - "'this is a test' in result.stdout_lines[0][1]" + +- name: teardown + cli_config: *rm + +- debug: msg="END cli_config/cli_comment.yaml on connection={{ ansible_connection }}"