Add cli_config module (#42413)
* cli_config module Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * rename diff and replace Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * add nxos changes Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * nxos tests Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * remove severity * address review comment Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * add module diff Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * add iosxr test Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * address diff review Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * Add junos tests Signed-off-by: Trishna Guha <trishnaguha17@gmail.com> * vyos cliconf diff fix Signed-off-by: Trishna Guha <trishnaguha17@gmail.com>
This commit is contained in:
parent
227bf61daa
commit
a8c24a5d5e
42 changed files with 960 additions and 4 deletions
326
lib/ansible/modules/network/cli/cli_config.py
Normal file
326
lib/ansible/modules/network/cli/cli_config.py
Normal file
|
@ -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()
|
31
lib/ansible/plugins/action/cli_config.py
Normal file
31
lib/ansible/plugins/action/cli_config.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
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)
|
|
@ -99,7 +99,8 @@ class Cliconf(CliconfBase):
|
||||||
else:
|
else:
|
||||||
self.send_command('exit')
|
self.send_command('exit')
|
||||||
|
|
||||||
resp['diff'] = diff_config
|
if diff_config:
|
||||||
|
resp['diff'] = diff_config
|
||||||
resp['response'] = results
|
resp['response'] = results
|
||||||
resp['request'] = requests
|
resp['request'] = requests
|
||||||
return resp
|
return resp
|
||||||
|
|
16
test/integration/targets/eos_config/tasks/cli_config.yaml
Normal file
16
test/integration/targets/eos_config/tasks/cli_config.yaml
Normal file
|
@ -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
|
|
@ -1,3 +1,4 @@
|
||||||
---
|
---
|
||||||
- { include: cli.yaml, tags: ['cli'] }
|
- { include: cli.yaml, tags: ['cli'] }
|
||||||
|
- { include: cli_config.yaml, tags: ['cli_config'] }
|
||||||
- { include: eapi.yaml, tags: ['eapi'] }
|
- { include: eapi.yaml, tags: ['eapi'] }
|
||||||
|
|
|
@ -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 }}"
|
16
test/integration/targets/ios_config/tasks/cli_config.yaml
Normal file
16
test/integration/targets/ios_config/tasks/cli_config.yaml
Normal file
|
@ -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
|
|
@ -1,2 +1,3 @@
|
||||||
---
|
---
|
||||||
- { include: cli.yaml, tags: ['cli'] }
|
- { include: cli.yaml, tags: ['cli'] }
|
||||||
|
- { include: cli_config.yaml, tags: ['cli_config'] }
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 }}"
|
|
@ -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 }}"
|
|
@ -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 }}"
|
|
@ -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 }}"
|
16
test/integration/targets/iosxr_config/tasks/cli_config.yaml
Normal file
16
test/integration/targets/iosxr_config/tasks/cli_config.yaml
Normal file
|
@ -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
|
|
@ -1,2 +1,3 @@
|
||||||
---
|
---
|
||||||
- { include: cli.yaml, tags: ['cli'] }
|
- { include: cli.yaml, tags: ['cli'] }
|
||||||
|
- { include: cli_config.yaml, tags: ['cli_config'] }
|
||||||
|
|
|
@ -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 }}"
|
16
test/integration/targets/junos_config/tasks/cli_config.yaml
Normal file
16
test/integration/targets/junos_config/tasks/cli_config.yaml
Normal file
|
@ -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
|
|
@ -1,2 +1,3 @@
|
||||||
---
|
---
|
||||||
- { include: netconf.yaml, tags: ['netconf'] }
|
- { include: netconf.yaml, tags: ['netconf'] }
|
||||||
|
- { include: cli_config.yaml, tags: ['cli_config'] }
|
||||||
|
|
|
@ -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 }}"
|
16
test/integration/targets/nxos_config/tasks/cli_config.yaml
Normal file
16
test/integration/targets/nxos_config/tasks/cli_config.yaml
Normal file
|
@ -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
|
|
@ -1,3 +1,4 @@
|
||||||
---
|
---
|
||||||
- { include: cli.yaml, tags: ['cli'] }
|
- { include: cli.yaml, tags: ['cli'] }
|
||||||
- { include: nxapi.yaml, tags: ['nxapi'] }
|
- { include: nxapi.yaml, tags: ['nxapi'] }
|
||||||
|
- { include: cli_config.yaml, tags: ['cli_config'] }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
interface Ethernet2/5
|
interface loopback1
|
||||||
description this is a test
|
description this is a test
|
||||||
shutdown
|
shutdown
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 }}"
|
|
@ -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 }}"
|
|
@ -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 }}"
|
|
@ -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 }}"
|
16
test/integration/targets/vyos_config/tasks/cli_config.yaml
Normal file
16
test/integration/targets/vyos_config/tasks/cli_config.yaml
Normal file
|
@ -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
|
|
@ -1,2 +1,3 @@
|
||||||
---
|
---
|
||||||
- { include: cli.yaml, tags: ['cli'] }
|
- { include: cli.yaml, tags: ['cli'] }
|
||||||
|
- { include: cli_config.yaml, tags: ['cli_config'] }
|
||||||
|
|
|
@ -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 }}"
|
|
@ -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 }}"
|
Loading…
Reference in a new issue