mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Update netconf_config module (#44379)
Fixes #40650 Fixes #40245 Fixes #41541 * Refactor netconf_config module as per proposal #104 * Update netconf_config module metadata to core network supported * Refactor local connection to use persistent connection framework for backward compatibility * Update netconf connection plugin configuration varaibles (Fixes #40245) * Add support for optional lock feature to Fixes #41541 * Add integration test for netconf_config module * Documentation update * Move deprecated options in netconf_config module
This commit is contained in:
parent
4632ae4b28
commit
ce541454e9
22 changed files with 805 additions and 268 deletions
|
@ -20,6 +20,7 @@ Some Ansible Network platforms support multiple connection types, privilege esca
|
|||
platform_routeros
|
||||
platform_slxos
|
||||
platform_voss
|
||||
platform_netconf_enabled
|
||||
|
||||
.. _settings_by_platform:
|
||||
|
||||
|
@ -63,5 +64,8 @@ Settings by Platform
|
|||
+-------------------+-------------------------+----------------------+----------------------+------------------+------------------+
|
||||
| VyOS* | ``vyos`` | in v. >=2.5 | N/A | N/A | in v. >=2.4 |
|
||||
+-------------------+-------------------------+----------------------+----------------------+------------------+------------------+
|
||||
| OS that supports | ``<network-os>`` | N/A | in v. >=2.6 | N/A | in v. >=2.2 |
|
||||
| Netconf* | | | | | |
|
||||
+-------------------+-------------------------+----------------------+----------------------+------------------+------------------+
|
||||
|
||||
`*` Maintained by Ansible Network Team
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
.. _netconf_enabled_platform_options:
|
||||
|
||||
***************************************
|
||||
Netconf enabled Platform Options
|
||||
***************************************
|
||||
|
||||
This page offers details on how the netconf connection works in Ansible 2.7 and how to use it.
|
||||
|
||||
.. contents:: Topics
|
||||
|
||||
Connections Available
|
||||
================================================================================
|
||||
|
||||
+----------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| | | | NETCONF |
|
||||
| | | | * all modules except ``junos_netconf``, which enables NETCONF |
|
||||
+============================+====================================================================================================+
|
||||
| **Protocol** | XML over SSH |
|
||||
+----------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| | **Credentials** | | uses SSH keys / SSH-agent if present |
|
||||
| | | | accepts ``-u myuser -k`` if using password |
|
||||
+----------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| **Indirect Access** | via a bastion (jump host) |
|
||||
+----------------------------+----------------------------------------------------------------------------------------------------+
|
||||
| **Connection Settings** | ``ansible_connection: netconf`` |
|
||||
+----------------------------+----------------------------------------------------------------------------------------------------+
|
||||
|
||||
For legacy playbooks, Ansible still supports ``ansible_connection=local`` for the netconf_config module only. We recommend modernizing to use ``ansible_connection=netconf`` as soon as possible.
|
||||
|
||||
Using NETCONF in Ansible 2.6 onwards
|
||||
================================================================================
|
||||
|
||||
Enabling NETCONF
|
||||
----------------
|
||||
|
||||
Before you can use NETCONF to connect to a switch, you must:
|
||||
|
||||
- install the ``ncclient`` Python package on your control node(s) with ``pip install ncclient``
|
||||
- enable NETCONF on the Junos OS device(s)
|
||||
|
||||
To enable NETCONF on a new switch via Ansible, use the platform specific module via the CLI connection or set it manually.
|
||||
For example set up your platform-level variables just like in the CLI example above, then run a playbook task like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- name: Enable NETCONF
|
||||
connection: network_cli
|
||||
junos_netconf:
|
||||
when: ansible_network_os == 'junos'
|
||||
|
||||
Once NETCONF is enabled, change your variables to use the NETCONF connection.
|
||||
|
||||
Example NETCONF inventory ``[junos:vars]``
|
||||
------------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
[junos:vars]
|
||||
ansible_connection=netconf
|
||||
ansible_network_os=junos
|
||||
ansible_user=myuser
|
||||
ansible_ssh_pass=!vault |
|
||||
|
||||
|
||||
Example NETCONF Task
|
||||
--------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- name: Backup current switch config
|
||||
netconf_config:
|
||||
backup: yes
|
||||
register: backup_junos_location
|
||||
|
||||
Example NETCONF Task with configurable variables
|
||||
------------------------------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- name: configure interface while providing different private key file path
|
||||
netconf_config:
|
||||
backup: yes
|
||||
register: backup_junos_location
|
||||
vars:
|
||||
ansible_private_key_file: /home/admin/.ssh/newprivatekeyfile
|
||||
|
||||
Note: For nectonf connection plugin configurable variables .. _Refer: https://docs.ansible.com/ansible/latest/plugins/connection/netconf.html
|
||||
|
||||
.. include:: shared_snippets/SSH_warning.txt
|
|
@ -1473,7 +1473,7 @@ MERGE_MULTIPLE_CLI_TAGS:
|
|||
version_added: "2.3"
|
||||
NETWORK_GROUP_MODULES:
|
||||
name: Network module families
|
||||
default: [eos, nxos, ios, iosxr, junos, enos, ce, vyos, sros, dellos9, dellos10, dellos6, asa, aruba, aireos, bigip, ironware, onyx]
|
||||
default: [eos, nxos, ios, iosxr, junos, enos, ce, vyos, sros, dellos9, dellos10, dellos6, asa, aruba, aireos, bigip, ironware, onyx, netconf]
|
||||
description: 'TODO: write it'
|
||||
env: [{name: NETWORK_GROUP_MODULES}]
|
||||
ini:
|
||||
|
|
|
@ -18,13 +18,22 @@
|
|||
#
|
||||
import json
|
||||
|
||||
from copy import deepcopy
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
try:
|
||||
from lxml.etree import fromstring, tostring
|
||||
except ImportError:
|
||||
from xml.etree.ElementTree import fromstring, tostring
|
||||
|
||||
from ansible.module_utils._text import to_text, to_bytes
|
||||
from ansible.module_utils.connection import Connection, ConnectionError
|
||||
from ansible.module_utils.network.common.netconf import NetconfConnection
|
||||
|
||||
|
||||
IGNORE_XML_ATTRIBUTE = ()
|
||||
|
||||
|
||||
def get_connection(module):
|
||||
if hasattr(module, '_netconf_connection'):
|
||||
return module._netconf_connection
|
||||
|
@ -114,3 +123,15 @@ def dispatch(module, request):
|
|||
module.fail_json(msg=to_text(e, errors='surrogate_then_replace').strip())
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def sanitize_xml(data):
|
||||
tree = fromstring(to_bytes(deepcopy(data), errors='surrogate_then_replace'))
|
||||
for element in tree.getiterator():
|
||||
# remove attributes
|
||||
attribute = element.attrib
|
||||
if attribute:
|
||||
for key in attribute:
|
||||
if key not in IGNORE_XML_ATTRIBUTE:
|
||||
attribute.pop(key)
|
||||
return to_text(tostring(tree), errors='surrogate_then_replace').strip()
|
||||
|
|
|
@ -9,112 +9,157 @@ __metaclass__ = type
|
|||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
'supported_by': 'network'}
|
||||
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: netconf_config
|
||||
version_added: "2.2"
|
||||
author: "Leandro Lisboa Penz (@lpenz)"
|
||||
short_description: netconf device configuration
|
||||
description:
|
||||
- Netconf is a network management protocol developed and standardized by
|
||||
the IETF. It is documented in RFC 6241.
|
||||
|
||||
- This module allows the user to send a configuration XML file to a netconf
|
||||
device, and detects if there was a configuration change.
|
||||
notes:
|
||||
- This module supports devices with and without the candidate and
|
||||
confirmed-commit capabilities. It always use the safer feature.
|
||||
- This module supports the use of connection=netconf
|
||||
version_added: "2.2"
|
||||
extends_documentation_fragment: netconf
|
||||
options:
|
||||
host:
|
||||
content:
|
||||
description:
|
||||
- the hostname or ip address of the netconf device
|
||||
required: true
|
||||
port:
|
||||
- The configuration data as defined by the device's data models, the value can be either in
|
||||
xml string format or text format. The format of the configuration should be supported by remote
|
||||
Netconf server
|
||||
aliases: ['xml']
|
||||
target:
|
||||
description:
|
||||
- the netconf port
|
||||
default: 830
|
||||
required: false
|
||||
hostkey_verify:
|
||||
description:
|
||||
- if true, the ssh host key of the device must match a ssh key present on the host
|
||||
- if false, the ssh host key of the device is not checked
|
||||
default: true
|
||||
required: false
|
||||
look_for_keys:
|
||||
description:
|
||||
- if true, enables looking in the usual locations for ssh keys (e.g. ~/.ssh/id_*)
|
||||
- if false, disables looking for ssh keys
|
||||
default: true
|
||||
required: false
|
||||
version_added: "2.4"
|
||||
allow_agent:
|
||||
description:
|
||||
- if true, enables querying SSH agent (if found) for keys
|
||||
- if false, disables querying the SSH agent for ssh keys
|
||||
default: true
|
||||
required: false
|
||||
version_added: "2.4"
|
||||
datastore:
|
||||
description:
|
||||
- auto, uses candidate and fallback to running
|
||||
- candidate, edit <candidate/> datastore and then commit
|
||||
- running, edit <running/> datastore directly
|
||||
Name of the configuration datastore to be edited.
|
||||
- auto, uses candidate and fallback to running
|
||||
- candidate, edit <candidate/> datastore and then commit
|
||||
- running, edit <running/> datastore directly
|
||||
default: auto
|
||||
required: false
|
||||
version_added: "2.4"
|
||||
aliases: ['datastore']
|
||||
source_datastore:
|
||||
description:
|
||||
- Name of the configuration datastore to use as the source to copy the configuration
|
||||
to the datastore mentioned by C(target) option. The values can be either I(running), I(candidate),
|
||||
I(startup) or a remote URL
|
||||
version_added: "2.7"
|
||||
aliases: ['source']
|
||||
format:
|
||||
description:
|
||||
- The format of the configuration provided as value of C(content). Accepted values are I(xml) and I(test) and
|
||||
the given configuration format should be supported by remote Netconf server.
|
||||
default: xml
|
||||
choices: ['xml', 'text']
|
||||
version_added: "2.7"
|
||||
lock:
|
||||
description:
|
||||
- Instructs the module to explicitly lock the datastore specified as C(target). By setting the option
|
||||
value I(always) is will explicitly lock the datastore mentioned in C(target) option. It the value
|
||||
is I(never) it will not lock the C(target) datastore. The value I(if-supported) lock the C(target)
|
||||
datastore only if it is supported by the remote Netconf server.
|
||||
default: always
|
||||
choices: ['never', 'always', 'if-supported']
|
||||
version_added: "2.7"
|
||||
default_operation:
|
||||
description:
|
||||
- The default operation for <edit-config> rpc, valid values are I(merge), I(replace) and I(none).
|
||||
If the default value is merge, the configuration data in the C(content) option is merged at the
|
||||
corresponding level in the C(target) datastore. If the value is replace the data in the C(content)
|
||||
option completely replaces the configuration in the C(target) datastore. If the value is none the C(target)
|
||||
datastore is unaffected by the configuration in the config option, unless and until the incoming configuration
|
||||
data uses the C(operation) operation to request a different operation.
|
||||
default: merge
|
||||
choices: ['merge', 'replace', 'none']
|
||||
version_added: "2.7"
|
||||
confirm:
|
||||
description:
|
||||
- This argument will configure a timeout value for the commit to be confirmed before it is automatically
|
||||
rolled back. If the C(confirm_commit) argument is set to False, this argument is silently ignored. If the
|
||||
value of this argument is set to 0, the commit is confirmed immediately. The remote host should
|
||||
support :candidate and :confirmed-commit capability for this option to .
|
||||
default: 0
|
||||
version_added: "2.7"
|
||||
confirm_commit:
|
||||
description:
|
||||
- This argument will execute commit operation on remote device. It can be used to confirm a previous commit.
|
||||
type: bool
|
||||
default: 'no'
|
||||
version_added: "2.7"
|
||||
error_option:
|
||||
description:
|
||||
- This option control the netconf server action after a error is occured while editing the configuration.
|
||||
If the value is I(stop-on-error) abort the config edit on first error, if value is I(continue-on-error)
|
||||
it continues to process configuration data on erro, error is recorded and negative response is generated
|
||||
if any errors occur. If value is C(rollback-on-error) it rollback to the original configuration in case
|
||||
any error occurs, this requires the remote Netconf server to support the :rollback-on-error capability.
|
||||
default: stop-on-error
|
||||
choices: ['stop-on-error', 'continue-on-error', 'rollback-on-error']
|
||||
version_added: "2.7"
|
||||
save:
|
||||
description:
|
||||
- The C(save) argument instructs the module to save the running-
|
||||
config to the startup-config if changed.
|
||||
required: false
|
||||
- The C(save) argument instructs the module to save the running-config to the startup-config if changed.
|
||||
default: false
|
||||
version_added: "2.4"
|
||||
username:
|
||||
backup:
|
||||
description:
|
||||
- the username to authenticate with
|
||||
required: true
|
||||
password:
|
||||
- This argument will cause the module to create a full backup of
|
||||
the current C(running-config) from the remote device before any
|
||||
changes are made. The backup file is written to the C(backup)
|
||||
folder in the playbook root directory or role root directory, if
|
||||
playbook is part of an ansible role. If the directory does not exist,
|
||||
it is created.
|
||||
type: bool
|
||||
default: 'no'
|
||||
version_added: "2.7"
|
||||
delete:
|
||||
description:
|
||||
- password of the user to authenticate with
|
||||
required: true
|
||||
xml:
|
||||
- It instructs the module to delete the configuration from value mentioned in C(target) datastore.
|
||||
type: bool
|
||||
default: 'no'
|
||||
version_added: "2.7"
|
||||
commit:
|
||||
description:
|
||||
- the XML content to send to the device
|
||||
required: false
|
||||
- This boolean flag controls if the configuration changes should be committed or not after editing the
|
||||
candidate datastore. This oprion is supported only if remote Netconf server supports :candidate
|
||||
capability. If the value is set to I(False) commit won't be issued after edit-config operation
|
||||
and user needs to handle commit or discard-changes explicitly.
|
||||
type: bool
|
||||
default: True
|
||||
version_added: "2.7"
|
||||
validate:
|
||||
description:
|
||||
- This boolean flag if set validates the content of datastore given in C(target) option.
|
||||
For this option to work remote Netconf server shoule support :validate capability.
|
||||
type: bool
|
||||
default: False
|
||||
version_added: "2.7"
|
||||
src:
|
||||
description:
|
||||
- Specifies the source path to the xml file that contains the configuration
|
||||
or configuration template to load. The path to the source file can
|
||||
either be the full path on the Ansible control host or a relative
|
||||
path from the playbook or role root directory. This argument is mutually
|
||||
exclusive with I(xml).
|
||||
required: false
|
||||
- Specifies the source path to the xml file that contains the configuration or configuration template
|
||||
to load. The path to the source file can either be the full path on the Ansible control host or
|
||||
a relative path from the playbook or role root directory. This argument is mutually exclusive with I(xml).
|
||||
version_added: "2.4"
|
||||
|
||||
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "ncclient"
|
||||
notes:
|
||||
- This module requires the netconf system service be enabled on
|
||||
the remote device being managed.
|
||||
- This module supports devices with and without the candidate and
|
||||
confirmed-commit capabilities. It will always use the safer feature.
|
||||
- This module supports the use of connection=netconf
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: use lookup filter to provide xml configuration
|
||||
netconf_config:
|
||||
xml: "{{ lookup('file', './config.xml') }}"
|
||||
host: 10.0.0.1
|
||||
username: admin
|
||||
password: admin
|
||||
content: "{{ lookup('file', './config.xml') }}"
|
||||
|
||||
- name: set ntp server in the device
|
||||
netconf_config:
|
||||
host: 10.0.0.1
|
||||
username: admin
|
||||
password: admin
|
||||
xml: |
|
||||
content: |
|
||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<system xmlns="urn:ietf:params:xml:ns:yang:ietf-system">
|
||||
<ntp>
|
||||
|
@ -129,10 +174,7 @@ EXAMPLES = '''
|
|||
|
||||
- name: wipe ntp configuration
|
||||
netconf_config:
|
||||
host: 10.0.0.1
|
||||
username: admin
|
||||
password: admin
|
||||
xml: |
|
||||
content: |
|
||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<system xmlns="urn:ietf:params:xml:ns:yang:ietf-system">
|
||||
<ntp>
|
||||
|
@ -144,6 +186,12 @@ EXAMPLES = '''
|
|||
</system>
|
||||
</config>
|
||||
|
||||
- name: configure interface while providing different private key file path (for connection=netconf)
|
||||
netconf_config:
|
||||
backup: yes
|
||||
register: backup_junos_location
|
||||
vars:
|
||||
ansible_private_key_file: /home/admin/.ssh/newprivatekeyfile
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
|
@ -152,191 +200,189 @@ server_capabilities:
|
|||
returned: success
|
||||
type: list
|
||||
sample: ['urn:ietf:params:netconf:base:1.1','urn:ietf:params:netconf:capability:confirmed-commit:1.0','urn:ietf:params:netconf:capability:candidate:1.0']
|
||||
|
||||
backup_path:
|
||||
description: The full path to the backup file
|
||||
returned: when backup is yes
|
||||
type: string
|
||||
sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34
|
||||
diff:
|
||||
description: If --diff option in enabled while running, the before and after configration change are
|
||||
returned as part of before and after key.
|
||||
returned: when diff is enabled
|
||||
type: string
|
||||
sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34
|
||||
'''
|
||||
|
||||
import traceback
|
||||
import xml.dom.minidom
|
||||
|
||||
from xml.etree.ElementTree import fromstring, tostring
|
||||
|
||||
try:
|
||||
import ncclient.manager
|
||||
HAS_NCCLIENT = True
|
||||
except ImportError:
|
||||
HAS_NCCLIENT = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils.connection import Connection, ConnectionError
|
||||
|
||||
|
||||
def netconf_edit_config(m, xml, commit, retkwargs, datastore, capabilities, local_connection):
|
||||
m.lock(target=datastore)
|
||||
try:
|
||||
if datastore == "candidate":
|
||||
m.discard_changes()
|
||||
|
||||
config_before = m.get_config(source=datastore)
|
||||
m.edit_config(target=datastore, config=xml)
|
||||
config_after = m.get_config(source=datastore)
|
||||
|
||||
if local_connection:
|
||||
changed = config_before.data_xml != config_after.data_xml
|
||||
else:
|
||||
changed = config_before != config_after
|
||||
|
||||
if changed and commit and datastore == "candidate":
|
||||
if ":confirmed-commit" in capabilities:
|
||||
m.commit(confirmed=True)
|
||||
m.commit()
|
||||
else:
|
||||
m.commit()
|
||||
|
||||
return changed
|
||||
finally:
|
||||
m.unlock(target=datastore)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- #
|
||||
# Main
|
||||
from ansible.module_utils.network.netconf.netconf import get_capabilities, get_config, sanitize_xml
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
xml=dict(type='str', required=False),
|
||||
src=dict(type='path', required=False),
|
||||
|
||||
datastore=dict(choices=['auto', 'candidate', 'running'], default='auto'),
|
||||
save=dict(type='bool', default=False),
|
||||
|
||||
# connection arguments
|
||||
host=dict(type='str'),
|
||||
port=dict(type='int', default=830),
|
||||
|
||||
username=dict(type='str', no_log=True),
|
||||
password=dict(type='str', no_log=True),
|
||||
|
||||
hostkey_verify=dict(type='bool', default=True),
|
||||
look_for_keys=dict(type='bool', default=True),
|
||||
|
||||
allow_agent=dict(type='bool', default=True),
|
||||
),
|
||||
mutually_exclusive=[('xml', 'src')]
|
||||
""" main entry point for module execution
|
||||
"""
|
||||
argument_spec = dict(
|
||||
content=dict(aliases=['xml']),
|
||||
target=dict(choices=['auto', 'candidate', 'running'], default='auto', aliases=['datastore']),
|
||||
source_datastore=dict(aliases=['source']),
|
||||
format=dict(choices=['xml', 'text'], default='xml'),
|
||||
lock=dict(choices=['never', 'always', 'if-supported'], default='always'),
|
||||
default_operation=dict(choices=['merge', 'replace', 'none'], default='merge'),
|
||||
confirm=dict(type='int', default=0),
|
||||
confirm_commit=dict(type='bool', default=False),
|
||||
error_option=dict(choices=['stop-on-error', 'continue-on-error', 'rollback-on-error'], default='stop-on-error'),
|
||||
backup=dict(type='bool', default=False),
|
||||
save=dict(type='bool', default=False),
|
||||
delete=dict(type='bool', default=False),
|
||||
commit=dict(type='bool', default=True),
|
||||
validate=dict(type='bool', default=False),
|
||||
)
|
||||
|
||||
if not module._socket_path and not HAS_NCCLIENT:
|
||||
module.fail_json(msg='could not import the python library '
|
||||
'ncclient required by this module')
|
||||
# deprecated options
|
||||
netconf_top_spec = {
|
||||
'src': dict(type='path', removed_in_version=2.11),
|
||||
'host': dict(removed_in_version=2.11),
|
||||
'port': dict(removed_in_version=2.11, type='int', default=830),
|
||||
'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME']), removed_in_version=2.11, no_log=True),
|
||||
'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), removed_in_version=2.11, no_log=True),
|
||||
'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), removed_in_version=2.11, type='path'),
|
||||
'hostkey_verify': dict(removed_in_version=2.11, type='bool', default=True),
|
||||
'look_for_keys': dict(removed_in_version=2.11, type='bool', default=True),
|
||||
'timeout': dict(removed_in_version=2.11, type='int', default=10),
|
||||
}
|
||||
argument_spec.update(netconf_top_spec)
|
||||
|
||||
if (module.params['src']):
|
||||
config_xml = str(module.params['src'])
|
||||
elif module.params['xml']:
|
||||
config_xml = str(module.params['xml'])
|
||||
else:
|
||||
module.fail_json(msg='Option src or xml must be provided')
|
||||
mutually_exclusive = [('content', 'src', 'source', 'delete', 'confirm_commit')]
|
||||
required_one_of = [('content', 'src', 'source', 'delete', 'confirm_commit')]
|
||||
|
||||
local_connection = module._socket_path is None
|
||||
module = AnsibleModule(argument_spec=argument_spec,
|
||||
required_one_of=required_one_of,
|
||||
mutually_exclusive=mutually_exclusive,
|
||||
supports_check_mode=True)
|
||||
|
||||
if not local_connection:
|
||||
m = Connection(module._socket_path)
|
||||
capabilities = module.from_json(m.get_capabilities())
|
||||
server_capabilities = capabilities.get('server_capabilities')
|
||||
if module.params['src']:
|
||||
module.deprecate(msg="argument 'src' has been deprecated. Use file lookup plugin instead to read file contents.",
|
||||
version="4 releases from v2.7")
|
||||
|
||||
else:
|
||||
nckwargs = dict(
|
||||
host=module.params['host'],
|
||||
port=module.params['port'],
|
||||
hostkey_verify=module.params['hostkey_verify'],
|
||||
allow_agent=module.params['allow_agent'],
|
||||
look_for_keys=module.params['look_for_keys'],
|
||||
username=module.params['username'],
|
||||
password=module.params['password'],
|
||||
)
|
||||
config = module.params['content'] or module.params['src']
|
||||
target = module.params['target']
|
||||
lock = module.params['lock']
|
||||
source = module.params['source']
|
||||
delete = module.params['delete']
|
||||
confirm_commit = module.params['confirm_commit']
|
||||
confirm = module.params['confirm']
|
||||
validate = module.params['validate']
|
||||
|
||||
try:
|
||||
m = ncclient.manager.connect(**nckwargs)
|
||||
server_capabilities = list(m.server_capabilities)
|
||||
except ncclient.transport.errors.AuthenticationError:
|
||||
module.fail_json(
|
||||
msg='authentication failed while connecting to device'
|
||||
)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='error connecting to the device: %s' % to_native(e), exception=traceback.format_exc())
|
||||
conn = Connection(module._socket_path)
|
||||
capabilities = get_capabilities(module)
|
||||
operations = capabilities['device_operations']
|
||||
|
||||
try:
|
||||
xml.dom.minidom.parseString(config_xml)
|
||||
except Exception as e:
|
||||
module.fail_json(msg='error parsing XML: %s' % to_native(e), exception=traceback.format_exc())
|
||||
supports_commit = operations.get('supports_commit', False)
|
||||
supports_writable_running = operations.get('supports_writable_running', False)
|
||||
supports_startup = operations.get('supports_startup', False)
|
||||
|
||||
retkwargs = dict()
|
||||
retkwargs['server_capabilities'] = server_capabilities
|
||||
|
||||
server_capabilities = '\n'.join(server_capabilities)
|
||||
|
||||
if module.params['datastore'] == 'candidate':
|
||||
if ':candidate' in server_capabilities:
|
||||
datastore = 'candidate'
|
||||
# identify target datastore
|
||||
if target == 'candidate' and not supports_commit:
|
||||
module.fail_json(msg=':candidate is not supported by this netconf server')
|
||||
elif target == 'running' and not supports_writable_running:
|
||||
module.fail_json(msg=':writable-running is not supported by this netconf server')
|
||||
elif target == 'auto':
|
||||
if supports_commit:
|
||||
target = 'candidate'
|
||||
elif supports_writable_running:
|
||||
target = 'running'
|
||||
else:
|
||||
if local_connection:
|
||||
m.close_session()
|
||||
module.fail_json(
|
||||
msg=':candidate is not supported by this netconf server'
|
||||
)
|
||||
elif module.params['datastore'] == 'running':
|
||||
if ':writable-running' in server_capabilities:
|
||||
datastore = 'running'
|
||||
else:
|
||||
if local_connection:
|
||||
m.close_session()
|
||||
module.fail_json(
|
||||
msg=':writable-running is not supported by this netconf server'
|
||||
)
|
||||
elif module.params['datastore'] == 'auto':
|
||||
if ':candidate' in server_capabilities:
|
||||
datastore = 'candidate'
|
||||
elif ':writable-running' in server_capabilities:
|
||||
datastore = 'running'
|
||||
else:
|
||||
if local_connection:
|
||||
m.close_session()
|
||||
module.fail_json(
|
||||
msg='neither :candidate nor :writable-running are supported by this netconf server'
|
||||
)
|
||||
module.fail_json(msg='neither :candidate nor :writable-running are supported by this netconf server')
|
||||
|
||||
# Netconf server capability validation against input options
|
||||
if module.params['save'] and not supports_startup:
|
||||
module.fail_json(msg='cannot copy <running/> to <startup/>, while :startup is not supported')
|
||||
|
||||
if module.params['confirm_commit'] and not operations.get('supports_confirm_commit', False):
|
||||
module.fail_json(msg='confirm commit is not supported by Netconf server')
|
||||
|
||||
if confirm_commit or (confirm > 0) and not operations.get('supports_confirm_commit', False):
|
||||
module.fail_json(msg='confirm commit is not supported by this netconf server')
|
||||
|
||||
if validate and not operations.get('supports_validate', False):
|
||||
module.fail_json(msg='validate is not supported by this netconf server')
|
||||
|
||||
if lock == 'never':
|
||||
execute_lock = False
|
||||
elif target in operations.get('lock_datastore', []):
|
||||
# lock is requested (always/if-support) and supported => lets do it
|
||||
execute_lock = True
|
||||
else:
|
||||
if local_connection:
|
||||
m.close_session()
|
||||
module.fail_json(
|
||||
msg=module.params['datastore'] + ' datastore is not supported by this ansible module'
|
||||
)
|
||||
|
||||
if module.params['save']:
|
||||
if ':startup' not in server_capabilities:
|
||||
module.fail_json(
|
||||
msg='cannot copy <running/> to <startup/>, while :startup is not supported'
|
||||
)
|
||||
# lock is requested (always/if-supported) but not supported => issue warning
|
||||
module.warn("lock operation on '%s' source is not supported on this device" % target)
|
||||
execute_lock = (lock == 'always')
|
||||
|
||||
result = {'changed': False, 'server_capabilities': capabilities.get('server_capabilities', [])}
|
||||
before = None
|
||||
locked = False
|
||||
try:
|
||||
changed = netconf_edit_config(
|
||||
m=m,
|
||||
xml=config_xml,
|
||||
commit=True,
|
||||
retkwargs=retkwargs,
|
||||
datastore=datastore,
|
||||
capabilities=server_capabilities,
|
||||
local_connection=local_connection
|
||||
)
|
||||
if changed and module.params['save']:
|
||||
m.copy_config(source="running", target="startup")
|
||||
except Exception as e:
|
||||
module.fail_json(msg='error editing configuration: %s' % to_native(e), exception=traceback.format_exc())
|
||||
finally:
|
||||
if local_connection:
|
||||
m.close_session()
|
||||
if module.params['backup']:
|
||||
response = get_config(module, target, lock=execute_lock)
|
||||
before = to_text(response, errors='surrogate_then_replace').strip()
|
||||
result['__backup__'] = before.strip()
|
||||
if validate:
|
||||
if not module.check_mode:
|
||||
conn.validate(target)
|
||||
if source:
|
||||
if not module.check_mode:
|
||||
conn.copy(source, target)
|
||||
result['changed'] = True
|
||||
elif delete:
|
||||
if not module.check_mode:
|
||||
conn.delete(target)
|
||||
result['changed'] = True
|
||||
elif confirm_commit:
|
||||
if not module.check_mode:
|
||||
conn.commit()
|
||||
result['changed'] = True
|
||||
else:
|
||||
if module.check_mode and not supports_commit:
|
||||
module.warn("check mode not supported as Netconf server doesn't support candidate capability")
|
||||
result['changed'] = True
|
||||
module.exit_json(**result)
|
||||
|
||||
module.exit_json(changed=changed, **retkwargs)
|
||||
if lock:
|
||||
conn.lock(target=target)
|
||||
locked = True
|
||||
if before is None:
|
||||
before = to_text(conn.get_config(source=target), errors='surrogate_then_replace').strip()
|
||||
|
||||
kwargs = {
|
||||
'target': target,
|
||||
'default_operation': module.params['default_operation'],
|
||||
'error_option': module.params['error_option'],
|
||||
'format': module.params['format'],
|
||||
}
|
||||
conn.edit_config(config, **kwargs)
|
||||
if supports_commit and module.params['commit']:
|
||||
if not module.check_mode:
|
||||
timeout = confirm if confirm > 0 else None
|
||||
conn.commit(confirmed=confirm_commit, timeout=timeout)
|
||||
else:
|
||||
conn.discard_changes()
|
||||
|
||||
after = to_text(conn.get_config(source='running'), errors='surrogate_then_replace').strip()
|
||||
|
||||
if sanitize_xml(before) != sanitize_xml(after):
|
||||
result['changed'] = True
|
||||
|
||||
if module._diff:
|
||||
if result['changed']:
|
||||
result['diff'] = {'before': before, 'after': after}
|
||||
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg=to_text(e, errors='surrogate_then_replace').strip())
|
||||
finally:
|
||||
if locked:
|
||||
conn.unlock(target=target)
|
||||
|
||||
module.exit_json(**result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
74
lib/ansible/plugins/action/netconf.py
Normal file
74
lib/ansible/plugins/action/netconf.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
#
|
||||
# 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
|
||||
|
||||
import copy
|
||||
import sys
|
||||
|
||||
from ansible.plugins.action.normal import ActionModule as _ActionModule
|
||||
|
||||
try:
|
||||
from __main__ import display
|
||||
except ImportError:
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
|
||||
class ActionModule(_ActionModule):
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
del tmp # tmp no longer has any effect
|
||||
|
||||
if self._play_context.connection not in ['netconf', 'local'] and self._task.action == 'netconf_config':
|
||||
return {'failed': True, 'msg': 'Connection type %s is not valid for netconf_config module. '
|
||||
'Valid connection type is netconf or local (deprecated)' % self._play_context.connection}
|
||||
elif self._play_context.connection not in ['netconf'] and self._task.action != 'netconf_config':
|
||||
return {'failed': True, 'msg': 'Connection type %s is not valid for %s module. '
|
||||
'Valid connection type is netconf.' % (self._play_context.connection, self._task.action)}
|
||||
|
||||
if self._play_context.connection == 'local' and self._task.action == 'netconf_config':
|
||||
args = self._task.args
|
||||
pc = copy.deepcopy(self._play_context)
|
||||
pc.connection = 'netconf'
|
||||
pc.port = int(args.get('port') or self._play_context.port or 830)
|
||||
|
||||
pc.remote_user = args.get('username') or self._play_context.connection_user
|
||||
pc.password = args.get('password') or self._play_context.password
|
||||
pc.private_key_file = args.get('ssh_keyfile') or self._play_context.private_key_file
|
||||
|
||||
display.vvv('using connection plugin %s (was local)' % pc.connection, pc.remote_addr)
|
||||
connection = self._shared_loader_obj.connection_loader.get('persistent', pc, sys.stdin)
|
||||
|
||||
timeout = args.get('timeout')
|
||||
command_timeout = int(timeout) if timeout else connection.get_option('persistent_command_timeout')
|
||||
connection.set_options(direct={'persistent_command_timeout': command_timeout, 'look_for_keys': args.get('look_for_keys'),
|
||||
'hostkey_verify': args.get('hostkey_verify'),
|
||||
'allow_agent': args.get('allow_agent')})
|
||||
|
||||
socket_path = connection.run()
|
||||
display.vvvv('socket_path: %s' % socket_path, pc.remote_addr)
|
||||
if not socket_path:
|
||||
return {'failed': True,
|
||||
'msg': 'unable to open shell. Please see: ' +
|
||||
'https://docs.ansible.com/ansible/network_debug_troubleshooting.html#unable-to-open-shell'}
|
||||
|
||||
task_vars['ansible_socket'] = socket_path
|
||||
|
||||
return super(ActionModule, self).run(task_vars=task_vars)
|
|
@ -19,9 +19,94 @@
|
|||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.plugins.action.net_config import ActionModule as NetActionModule
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import glob
|
||||
|
||||
from ansible.plugins.action.netconf import ActionModule as _ActionModule
|
||||
from ansible.module_utils._text import to_text, to_bytes
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlsplit
|
||||
|
||||
PRIVATE_KEYS_RE = re.compile('__.+__')
|
||||
|
||||
|
||||
class ActionModule(NetActionModule, ActionBase):
|
||||
pass
|
||||
class ActionModule(_ActionModule):
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
if self._task.args.get('src'):
|
||||
try:
|
||||
self._handle_template()
|
||||
except ValueError as exc:
|
||||
return dict(failed=True, msg=to_text(exc))
|
||||
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
del tmp # tmp no longer has any effect
|
||||
|
||||
if self._task.args.get('backup') and result.get('__backup__'):
|
||||
# User requested backup and no error occurred in module.
|
||||
# NOTE: If there is a parameter error, _backup key may not be in results.
|
||||
filepath = self._write_backup(task_vars['inventory_hostname'],
|
||||
result['__backup__'])
|
||||
|
||||
result['backup_path'] = filepath
|
||||
|
||||
# strip out any keys that have two leading and two trailing
|
||||
# underscore characters
|
||||
for key in list(result):
|
||||
if PRIVATE_KEYS_RE.match(key):
|
||||
del result[key]
|
||||
|
||||
return result
|
||||
|
||||
def _get_working_path(self):
|
||||
cwd = self._loader.get_basedir()
|
||||
if self._task._role is not None:
|
||||
cwd = self._task._role._role_path
|
||||
return cwd
|
||||
|
||||
def _write_backup(self, host, contents):
|
||||
backup_path = self._get_working_path() + '/backup'
|
||||
if not os.path.exists(backup_path):
|
||||
os.mkdir(backup_path)
|
||||
for fn in glob.glob('%s/%s*' % (backup_path, host)):
|
||||
os.remove(fn)
|
||||
tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time()))
|
||||
filename = '%s/%s_config.%s' % (backup_path, host, tstamp)
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(to_bytes(to_text(contents, encoding='latin-1'), encoding='utf-8'))
|
||||
return filename
|
||||
|
||||
def _handle_template(self):
|
||||
src = self._task.args.get('src')
|
||||
working_path = self._get_working_path()
|
||||
|
||||
if os.path.isabs(src) or urlsplit('src').scheme:
|
||||
source = src
|
||||
else:
|
||||
source = self._loader.path_dwim_relative(working_path, 'templates', src)
|
||||
if not source:
|
||||
source = self._loader.path_dwim_relative(working_path, src)
|
||||
|
||||
if not os.path.exists(source):
|
||||
raise ValueError('path specified in src not found')
|
||||
|
||||
try:
|
||||
with open(source, 'r') as f:
|
||||
template_data = to_text(f.read())
|
||||
except IOError:
|
||||
return dict(failed=True, msg='unable to load src file')
|
||||
|
||||
# Create a template search path in the following order:
|
||||
# [working_path, self_role_path, dependent_role_paths, dirname(source)]
|
||||
searchpath = [working_path]
|
||||
if self._task._role is not None:
|
||||
searchpath.append(self._task._role._role_path)
|
||||
if hasattr(self._task, "_block:"):
|
||||
dep_chain = self._task._block.get_dep_chain()
|
||||
if dep_chain is not None:
|
||||
for role in dep_chain:
|
||||
searchpath.append(role._role_path)
|
||||
searchpath.append(os.path.dirname(source))
|
||||
self._templar.environment.loader.searchpath = searchpath
|
||||
self._task.args['src'] = self._templar.template(template_data)
|
||||
|
|
|
@ -102,7 +102,8 @@ options:
|
|||
- name: ANSIBLE_HOST_KEY_AUTO_ADD
|
||||
look_for_keys:
|
||||
default: True
|
||||
description: 'TODO: write it'
|
||||
description:
|
||||
- enables looking for ssh keys in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`)
|
||||
env:
|
||||
- name: ANSIBLE_PARAMIKO_LOOK_FOR_KEYS
|
||||
ini:
|
||||
|
@ -218,6 +219,7 @@ class Connection(NetworkConnectionBase):
|
|||
display.display('network_os is set to %s' % self._network_os, log_only=True)
|
||||
|
||||
self._manager = None
|
||||
self.key_filename = None
|
||||
|
||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||||
"""Sends the request to the node and returns the reply
|
||||
|
@ -252,9 +254,9 @@ class Connection(NetworkConnectionBase):
|
|||
allow_agent = False
|
||||
setattr(self._play_context, 'allow_agent', allow_agent)
|
||||
|
||||
key_filename = None
|
||||
if self._play_context.private_key_file:
|
||||
key_filename = os.path.expanduser(self._play_context.private_key_file)
|
||||
self.key_filename = self._play_context.private_key_file or self.get_option('private_key_file')
|
||||
if self.key_filename:
|
||||
self.key_filename = os.path.expanduser(self.key_filename)
|
||||
|
||||
if self._network_os == 'default':
|
||||
for cls in netconf_loader.all(class_only=True):
|
||||
|
@ -277,7 +279,7 @@ class Connection(NetworkConnectionBase):
|
|||
port=self._play_context.port or 830,
|
||||
username=self._play_context.remote_user,
|
||||
password=self._play_context.password,
|
||||
key_filename=str(key_filename),
|
||||
key_filename=str(self.key_filename),
|
||||
hostkey_verify=self.get_option('host_key_checking'),
|
||||
look_for_keys=self.get_option('look_for_keys'),
|
||||
device_params=device_params,
|
||||
|
|
|
@ -111,8 +111,6 @@ class NetconfBase(AnsiblePlugin):
|
|||
:param name: Name of rpc in string format
|
||||
:return: Received rpc response from remote host
|
||||
"""
|
||||
"""RPC to be execute on remote device
|
||||
:name: Name of rpc in string format"""
|
||||
try:
|
||||
obj = to_ele(name)
|
||||
resp = self.m.rpc(obj)
|
||||
|
@ -275,13 +273,19 @@ class NetconfBase(AnsiblePlugin):
|
|||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@ensure_connected
|
||||
def locked(self, target):
|
||||
def delete_config(self, target):
|
||||
"""
|
||||
Returns a context manager for a lock on a datastore
|
||||
:param target: Name of the configuration datastore to lock
|
||||
:return: Locked context object
|
||||
delete a configuration datastore
|
||||
:param target: specifies the name or URL of configuration datastore to delete
|
||||
:return: Returns xml string containing the RPC response received from remote host
|
||||
"""
|
||||
return self.m.locked(target)
|
||||
resp = self.m.delete_config(target)
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@ensure_connected
|
||||
def locked(self, *args, **kwargs):
|
||||
resp = self.m.locked(*args, **kwargs)
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@abstractmethod
|
||||
def get_capabilities(self):
|
||||
|
@ -341,6 +345,7 @@ class NetconfBase(AnsiblePlugin):
|
|||
operations['supports_startup'] = ':startup' in capabilities
|
||||
operations['supports_xpath'] = ':xpath' in capabilities
|
||||
operations['supports_writable_running'] = ':writable-running' in capabilities
|
||||
operations['supports_validate'] = ':writable-validate' in capabilities
|
||||
|
||||
operations['lock_datastore'] = []
|
||||
if operations['supports_writable_running']:
|
||||
|
|
|
@ -109,9 +109,9 @@ class Netconf(NetconfBase):
|
|||
port=obj._play_context.port or 830,
|
||||
username=obj._play_context.remote_user,
|
||||
password=obj._play_context.password,
|
||||
key_filename=obj._play_context.private_key_file,
|
||||
hostkey_verify=C.HOST_KEY_CHECKING,
|
||||
look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS,
|
||||
key_filename=obj.key_filename,
|
||||
hostkey_verify=obj.get_option('host_key_checking'),
|
||||
look_for_keys=obj.get_option('look_for_keys'),
|
||||
allow_agent=obj._play_context.allow_agent,
|
||||
timeout=obj._play_context.timeout
|
||||
)
|
||||
|
|
|
@ -104,9 +104,9 @@ class Netconf(NetconfBase):
|
|||
port=obj._play_context.port or 830,
|
||||
username=obj._play_context.remote_user,
|
||||
password=obj._play_context.password,
|
||||
key_filename=obj._play_context.private_key_file,
|
||||
hostkey_verify=C.HOST_KEY_CHECKING,
|
||||
look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS,
|
||||
key_filename=obj.key_filename,
|
||||
hostkey_verify=obj.get_option('host_key_checking'),
|
||||
look_for_keys=obj.get_option('look_for_keys'),
|
||||
allow_agent=obj._play_context.allow_agent,
|
||||
timeout=obj._play_context.timeout
|
||||
)
|
||||
|
|
|
@ -113,9 +113,9 @@ class Netconf(NetconfBase):
|
|||
port=obj._play_context.port or 830,
|
||||
username=obj._play_context.remote_user,
|
||||
password=obj._play_context.password,
|
||||
key_filename=obj._play_context.private_key_file,
|
||||
hostkey_verify=C.HOST_KEY_CHECKING,
|
||||
look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS,
|
||||
key_filename=obj.key_filename,
|
||||
hostkey_verify=obj.get_option('host_key_checking'),
|
||||
look_for_keys=obj.get_option('look_for_keys'),
|
||||
allow_agent=obj._play_context.allow_agent,
|
||||
timeout=obj._play_context.timeout
|
||||
)
|
||||
|
|
|
@ -82,9 +82,9 @@ class Netconf(NetconfBase):
|
|||
port=obj._play_context.port or 830,
|
||||
username=obj._play_context.remote_user,
|
||||
password=obj._play_context.password,
|
||||
key_filename=obj._play_context.private_key_file,
|
||||
hostkey_verify=C.HOST_KEY_CHECKING,
|
||||
look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS,
|
||||
key_filename=obj.key_filename,
|
||||
hostkey_verify=obj.get_option('host_key_checking'),
|
||||
look_for_keys=obj.get_option('look_for_keys'),
|
||||
allow_agent=obj._play_context.allow_agent,
|
||||
timeout=obj._play_context.timeout
|
||||
)
|
||||
|
|
77
lib/ansible/utils/module_docs_fragments/netconf.py
Normal file
77
lib/ansible/utils/module_docs_fragments/netconf.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
#
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard files documentation fragment
|
||||
DOCUMENTATION = """
|
||||
options:
|
||||
host:
|
||||
description:
|
||||
- Specifies the DNS host name or address for connecting to the remote
|
||||
device over the specified transport. The value of host is used as
|
||||
the destination address for the transport.
|
||||
required: true
|
||||
port:
|
||||
description:
|
||||
- Specifies the port to use when building the connection to the remote
|
||||
device. The port value will default to port 830.
|
||||
type: int
|
||||
default: 830
|
||||
username:
|
||||
description:
|
||||
- Configures the username to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_USERNAME) will be used instead.
|
||||
password:
|
||||
description:
|
||||
- Specifies the password to use to authenticate the connection to
|
||||
the remote device. This value is used to authenticate
|
||||
the SSH session. If the value is not specified in the task, the
|
||||
value of environment variable C(ANSIBLE_NET_PASSWORD) will be used instead.
|
||||
timeout:
|
||||
description:
|
||||
- Specifies the timeout in seconds for communicating with the network device
|
||||
for either connecting or sending commands. If the timeout is
|
||||
exceeded before the operation is completed, the module will error.
|
||||
type: int
|
||||
default: 10
|
||||
ssh_keyfile:
|
||||
description:
|
||||
- Specifies the SSH key to use to authenticate the connection to
|
||||
the remote device. This value is the path to the key
|
||||
used to authenticate the SSH session. If the value is not specified in
|
||||
the task, the value of environment variable C(ANSIBLE_NET_SSH_KEYFILE)
|
||||
will be used instead.
|
||||
type: path
|
||||
hostkey_verify:
|
||||
description:
|
||||
- If set to true, the ssh host key of the device must match a ssh key present on
|
||||
the host if false, the ssh host key of the device is not checked.
|
||||
type: bool
|
||||
default: True
|
||||
look_for_keys:
|
||||
description:
|
||||
- Enables looking in the usual locations for the ssh keys (e.g. :file:`~/.ssh/id_*`)
|
||||
type: bool
|
||||
default: True
|
||||
notes:
|
||||
- For information on using netconf see the :ref:`Platform Options guide using Netconf<netconf_platform_enabled_options>`
|
||||
- For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide <network_guide>`
|
||||
"""
|
|
@ -0,0 +1,2 @@
|
|||
---
|
||||
testcase: "*"
|
4
test/integration/targets/netconf_config/meta/main.yml
Normal file
4
test/integration/targets/netconf_config/meta/main.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
dependencies:
|
||||
- { role: prepare_junos_tests, when: ansible_network_os == 'junos' }
|
||||
- { role: prepare_iosxr_tests, when: ansible_network_os == 'iosxr' }
|
16
test/integration/targets/netconf_config/tasks/iosxr.yaml
Normal file
16
test/integration/targets/netconf_config/tasks/iosxr.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
- name: collect all netconf test cases
|
||||
find:
|
||||
paths: "{{ role_path }}/tests/iosxr"
|
||||
patterns: "{{ testcase }}.yaml"
|
||||
register: test_cases
|
||||
connection: local
|
||||
|
||||
- name: set test_items
|
||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
||||
|
||||
- name: run test case (connection=netconf)
|
||||
include: "{{ test_case_to_run }}"
|
||||
with_items: "{{ test_items }}"
|
||||
loop_control:
|
||||
loop_var: test_case_to_run
|
16
test/integration/targets/netconf_config/tasks/junos.yaml
Normal file
16
test/integration/targets/netconf_config/tasks/junos.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
- name: collect all netconf test cases
|
||||
find:
|
||||
paths: "{{ role_path }}/tests/junos"
|
||||
patterns: "{{ testcase }}.yaml"
|
||||
register: test_cases
|
||||
connection: local
|
||||
|
||||
- name: set test_items
|
||||
set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}"
|
||||
|
||||
- name: run test case (connection=netconf)
|
||||
include: "{{ test_case_to_run }} ansible_connection=netconf"
|
||||
with_items: "{{ test_items }}"
|
||||
loop_control:
|
||||
loop_var: test_case_to_run
|
3
test/integration/targets/netconf_config/tasks/main.yaml
Normal file
3
test/integration/targets/netconf_config/tasks/main.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
- { include: junos.yaml, when: ansible_network_os == 'junos', tags: ['netconf'] }
|
||||
- { include: iosxr.yaml, when: ansible_network_os == 'iosxr', tags: ['netconf'] }
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
- debug: msg="START netconf_config junos/basic.yaml on connection={{ ansible_connection }}"
|
||||
|
||||
- include_vars: "{{playbook_dir }}/targets/netconf_config/tests/junos/fixtures/config.yml"
|
||||
|
||||
- name: syslog file config- setup
|
||||
junos_config:
|
||||
lines:
|
||||
- delete system syslog file test_netconf_config
|
||||
|
||||
- name: configure syslog file
|
||||
netconf_config:
|
||||
content: "{{ syslog_config }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result.changed == true"
|
||||
- "'<name>test_netconf_config</name>' in result.diff.after"
|
||||
|
||||
- name: configure syslog file (idempotent)
|
||||
netconf_config:
|
||||
content: "{{ syslog_config }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result.changed == false"
|
||||
|
||||
- name: configure syslog file replace
|
||||
netconf_config:
|
||||
content: "{{ syslog_config_replace }}"
|
||||
default_operation: 'replace'
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "result.changed == true"
|
||||
|
||||
- name: test backup
|
||||
netconf_config:
|
||||
content: "{{ syslog_config }}"
|
||||
backup: True
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- "'backup_path' in result"
|
||||
|
||||
- name: syslog file config- teardown
|
||||
junos_config:
|
||||
lines:
|
||||
- delete system syslog file test_netconf_config
|
||||
|
||||
- debug: msg="END netconf_config junos/basic.yaml on connection={{ ansible_connection }}"
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
syslog_config: |
|
||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<configuration>
|
||||
<system>
|
||||
<syslog>
|
||||
<file>
|
||||
<name>test_netconf_config</name>
|
||||
<contents>
|
||||
<name>any</name>
|
||||
<any/>
|
||||
</contents>
|
||||
<contents>
|
||||
<name>kernel</name>
|
||||
<critical/>
|
||||
</contents>
|
||||
</file>
|
||||
</syslog>
|
||||
</system>
|
||||
</configuration>
|
||||
</config>
|
||||
|
||||
syslog_config_replace: |
|
||||
<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0">
|
||||
<configuration>
|
||||
<system>
|
||||
<syslog>
|
||||
<file>
|
||||
<name>test_netconf_config</name>
|
||||
<contents>
|
||||
<name>any</name>
|
||||
<any/>
|
||||
</contents>
|
||||
</file>
|
||||
</syslog>
|
||||
</system>
|
||||
</configuration>
|
||||
</config>
|
Loading…
Reference in a new issue