mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-05-07 13:52:54 +00:00
Add netconf_get module (#39869)
* Add netconf_get module Implements part-1 of proposal #104 https://github.com/ansible/proposals/issues/104 * Add netconf_get module * Refactor `get`, `get_config`, `lock`, `unlock` and `discard_changes` netconf plugin api's * Add netconf module_utils file which netconf module related common functions * Refactor junos and iosxr netconf plugins * Fix source option handling * Fix review comments * Update botmeta file * Update review comments and add support for lock * Lock update fix * Fix CI issue * Add integration test and minor fixes * Fix review comments * Fix CI failure * Fix CI issues * Fix CI issues * Fix review comments and update integration test * Fix review comments * Fix review comments * Fix review comments Fix reveiw comments
This commit is contained in:
@@ -110,22 +110,35 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
|
||||
raise Exception(to_xml(msg))
|
||||
|
||||
@ensure_connected
|
||||
def get_config(self, *args, **kwargs):
|
||||
"""Retrieve all or part of a specified configuration.
|
||||
:source: name of the configuration datastore being queried
|
||||
:filter: specifies the portion of the configuration to retrieve
|
||||
(by default entire configuration is retrieved)"""
|
||||
resp = self.m.get_config(*args, **kwargs)
|
||||
def get_config(self, source=None, filter=None):
|
||||
"""Retrieve all or part of a specified configuration
|
||||
(by default entire configuration is retrieved).
|
||||
|
||||
:param source: Name of the configuration datastore being queried, defaults to running datastore
|
||||
:param filter: This argument specifies the portion of the configuration data to retrieve
|
||||
:return: Returns xml string containing the RPC response received from remote host
|
||||
"""
|
||||
if isinstance(filter, list):
|
||||
filter = tuple(filter)
|
||||
|
||||
if not source:
|
||||
source = 'running'
|
||||
resp = self.m.get_config(source=source, filter=filter)
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@ensure_connected
|
||||
def get(self, *args, **kwargs):
|
||||
"""Retrieve running configuration and device state information.
|
||||
*filter* specifies the portion of the configuration to retrieve
|
||||
(by default entire configuration is retrieved)
|
||||
def get(self, filter=None):
|
||||
"""Retrieve device configuration and state information.
|
||||
|
||||
:param filter: This argument specifies the portion of the state data to retrieve
|
||||
(by default entire state data is retrieved)
|
||||
:return: Returns xml string containing the RPC response received from remote host
|
||||
"""
|
||||
resp = self.m.get(*args, **kwargs)
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
if isinstance(filter, list):
|
||||
filter = tuple(filter)
|
||||
resp = self.m.get(filter=filter)
|
||||
response = resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
return response
|
||||
|
||||
@ensure_connected
|
||||
def edit_config(self, *args, **kwargs):
|
||||
@@ -162,26 +175,42 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@ensure_connected
|
||||
def lock(self, *args, **kwargs):
|
||||
"""Allows the client to lock the configuration system of a device.
|
||||
*target* is the name of the configuration datastore to lock
|
||||
def lock(self, target=None):
|
||||
"""
|
||||
resp = self.m.lock(*args, **kwargs)
|
||||
Allows the client to lock the configuration system of a device.
|
||||
:param target: is the name of the configuration datastore to lock,
|
||||
defaults to candidate datastore
|
||||
:return: Returns xml string containing the RPC response received from remote host
|
||||
"""
|
||||
if not target:
|
||||
target = 'candidate'
|
||||
resp = self.m.lock(target=target)
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@ensure_connected
|
||||
def unlock(self, *args, **kwargs):
|
||||
def unlock(self, target=None):
|
||||
"""
|
||||
Release a configuration lock, previously obtained with the lock operation.
|
||||
:param target: is the name of the configuration datastore to unlock,
|
||||
defaults to candidate datastore
|
||||
:return: Returns xml string containing the RPC response received from remote host
|
||||
"""
|
||||
"""Release a configuration lock, previously obtained with the lock operation.
|
||||
:target: is the name of the configuration datastore to unlock
|
||||
"""
|
||||
resp = self.m.unlock(*args, **kwargs)
|
||||
if not target:
|
||||
target = 'candidate'
|
||||
resp = self.m.unlock(target=target)
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@ensure_connected
|
||||
def discard_changes(self, *args, **kwargs):
|
||||
"""Revert the candidate configuration to the currently running configuration.
|
||||
Any uncommitted changes are discarded."""
|
||||
resp = self.m.discard_changes(*args, **kwargs)
|
||||
def discard_changes(self):
|
||||
"""
|
||||
Revert the candidate configuration to the currently running configuration.
|
||||
Any uncommitted changes are discarded.
|
||||
:return: Returns xml string containing the RPC response received from remote host
|
||||
"""
|
||||
resp = self.m.discard_changes()
|
||||
return resp.data_xml if hasattr(resp, 'data_xml') else resp.xml
|
||||
|
||||
@ensure_connected
|
||||
@@ -245,4 +274,28 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
|
||||
"""Fetch file over scp from remote device"""
|
||||
pass
|
||||
|
||||
def get_device_operations(self, server_capabilities):
|
||||
operations = {}
|
||||
capabilities = '\n'.join(server_capabilities)
|
||||
operations['supports_commit'] = True if ':candidate' in capabilities else False
|
||||
operations['supports_defaults'] = True if ':with-defaults' in capabilities else False
|
||||
operations['supports_confirm_commit'] = True if ':confirmed-commit' in capabilities else False
|
||||
operations['supports_startup'] = True if ':startup' in capabilities else False
|
||||
operations['supports_xpath'] = True if ':xpath' in capabilities else False
|
||||
operations['supports_writeable_running'] = True if ':writable-running' in capabilities else False
|
||||
|
||||
operations['lock_datastore'] = []
|
||||
if operations['supports_writeable_running']:
|
||||
operations['lock_datastore'].append('running')
|
||||
|
||||
if operations['supports_commit']:
|
||||
operations['lock_datastore'].append('candidate')
|
||||
|
||||
if operations['supports_startup']:
|
||||
operations['lock_datastore'].append('startup')
|
||||
|
||||
operations['supports_lock'] = True if len(operations['lock_datastore']) else False
|
||||
|
||||
return operations
|
||||
|
||||
# TODO Restore .xml, when ncclient supports it for all platforms
|
||||
|
||||
@@ -48,4 +48,5 @@ class Netconf(NetconfBase):
|
||||
result['server_capabilities'] = [c for c in self.m.server_capabilities]
|
||||
result['client_capabilities'] = [c for c in self.m.client_capabilities]
|
||||
result['session_id'] = self.m.session_id
|
||||
result['device_operations'] = self.get_device_operations(result['server_capabilities'])
|
||||
return json.dumps(result)
|
||||
|
||||
@@ -22,12 +22,10 @@ __metaclass__ = type
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import collections
|
||||
from io import BytesIO
|
||||
from ansible.module_utils.six import StringIO
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.module_utils.network.common.netconf import remove_namespaces
|
||||
from ansible.module_utils.network.iosxr.iosxr import build_xml
|
||||
from ansible.errors import AnsibleConnectionFailure, AnsibleError
|
||||
from ansible.plugins.netconf import NetconfBase
|
||||
@@ -47,45 +45,6 @@ except ImportError:
|
||||
raise AnsibleError("lxml is not installed")
|
||||
|
||||
|
||||
def transform_reply():
|
||||
reply = '''<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="xml" indent="no"/>
|
||||
|
||||
<xsl:template match="/|comment()|processing-instruction()">
|
||||
<xsl:copy>
|
||||
<xsl:apply-templates/>
|
||||
</xsl:copy>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="*">
|
||||
<xsl:element name="{local-name()}">
|
||||
<xsl:apply-templates select="@*|node()"/>
|
||||
</xsl:element>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="@*">
|
||||
<xsl:attribute name="{local-name()}">
|
||||
<xsl:value-of select="."/>
|
||||
</xsl:attribute>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
'''
|
||||
if sys.version < '3':
|
||||
return reply
|
||||
else:
|
||||
return reply.encode('UTF-8')
|
||||
|
||||
|
||||
# Note: Workaround for ncclient 0.5.3
|
||||
def remove_namespaces(rpc_reply):
|
||||
xslt = transform_reply()
|
||||
parser = etree.XMLParser(remove_blank_text=True)
|
||||
xslt_doc = etree.parse(BytesIO(xslt), parser)
|
||||
transform = etree.XSLT(xslt_doc)
|
||||
|
||||
return etree.fromstring(str(transform(etree.parse(StringIO(str(rpc_reply))))))
|
||||
|
||||
|
||||
class Netconf(NetconfBase):
|
||||
|
||||
@ensure_connected
|
||||
@@ -129,7 +88,7 @@ class Netconf(NetconfBase):
|
||||
result['server_capabilities'] = [c for c in self.m.server_capabilities]
|
||||
result['client_capabilities'] = [c for c in self.m.client_capabilities]
|
||||
result['session_id'] = self.m.session_id
|
||||
|
||||
result['device_operations'] = self.get_device_operations(result['server_capabilities'])
|
||||
return json.dumps(result)
|
||||
|
||||
@staticmethod
|
||||
@@ -161,18 +120,22 @@ class Netconf(NetconfBase):
|
||||
|
||||
# TODO: change .xml to .data_xml, when ncclient supports data_xml on all platforms
|
||||
@ensure_connected
|
||||
def get(self, *args, **kwargs):
|
||||
def get(self, filter=None):
|
||||
if isinstance(filter, list):
|
||||
filter = tuple(filter)
|
||||
try:
|
||||
response = self.m.get(*args, **kwargs)
|
||||
return to_xml(remove_namespaces(response))
|
||||
response = self.m.get(filter=filter)
|
||||
return remove_namespaces(response)
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@ensure_connected
|
||||
def get_config(self, *args, **kwargs):
|
||||
def get_config(self, source=None, filter=None):
|
||||
if isinstance(filter, list):
|
||||
filter = tuple(filter)
|
||||
try:
|
||||
response = self.m.get_config(*args, **kwargs)
|
||||
return to_xml(remove_namespaces(response))
|
||||
response = self.m.get_config(source=source, filter=filter)
|
||||
return remove_namespaces(response)
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@@ -180,7 +143,7 @@ class Netconf(NetconfBase):
|
||||
def edit_config(self, *args, **kwargs):
|
||||
try:
|
||||
response = self.m.edit_config(*args, **kwargs)
|
||||
return to_xml(remove_namespaces(response))
|
||||
return remove_namespaces(response)
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@@ -188,7 +151,7 @@ class Netconf(NetconfBase):
|
||||
def commit(self, *args, **kwargs):
|
||||
try:
|
||||
response = self.m.commit(*args, **kwargs)
|
||||
return to_xml(remove_namespaces(response))
|
||||
return remove_namespaces(response)
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@@ -196,14 +159,14 @@ class Netconf(NetconfBase):
|
||||
def validate(self, *args, **kwargs):
|
||||
try:
|
||||
response = self.m.validate(*args, **kwargs)
|
||||
return to_xml(remove_namespaces(response))
|
||||
return remove_namespaces(response)
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@ensure_connected
|
||||
def discard_changes(self, *args, **kwargs):
|
||||
def discard_changes(self):
|
||||
try:
|
||||
response = self.m.discard_changes(*args, **kwargs)
|
||||
return to_xml(remove_namespaces(response))
|
||||
response = self.m.discard_changes()
|
||||
return remove_namespaces(response)
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@@ -92,6 +92,7 @@ class Netconf(NetconfBase):
|
||||
result['server_capabilities'] = [c for c in self.m.server_capabilities]
|
||||
result['client_capabilities'] = [c for c in self.m.client_capabilities]
|
||||
result['session_id'] = self.m.session_id
|
||||
result['device_operations'] = self.get_device_operations(result['server_capabilities'])
|
||||
return json.dumps(result)
|
||||
|
||||
@staticmethod
|
||||
@@ -143,44 +144,3 @@ class Netconf(NetconfBase):
|
||||
def reboot(self):
|
||||
"""reboot the device"""
|
||||
return self.m.reboot().data_xml
|
||||
|
||||
@ensure_connected
|
||||
def halt(self):
|
||||
"""reboot the device"""
|
||||
return self.m.halt().data_xml
|
||||
|
||||
@ensure_connected
|
||||
def get(self, *args, **kwargs):
|
||||
try:
|
||||
return self.m.get(*args, **kwargs).data_xml
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@ensure_connected
|
||||
def get_config(self, *args, **kwargs):
|
||||
try:
|
||||
return self.m.get_config(*args, **kwargs).data_xml
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@ensure_connected
|
||||
def edit_config(self, *args, **kwargs):
|
||||
try:
|
||||
self.m.edit_config(*args, **kwargs).data_xml
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@ensure_connected
|
||||
def commit(self, *args, **kwargs):
|
||||
try:
|
||||
return self.m.commit(*args, **kwargs).data_xml
|
||||
except RPCError as exc:
|
||||
raise Exception(to_xml(exc.xml))
|
||||
|
||||
@ensure_connected
|
||||
def validate(self, *args, **kwargs):
|
||||
return self.m.validate(*args, **kwargs).data_xml
|
||||
|
||||
@ensure_connected
|
||||
def discard_changes(self, *args, **kwargs):
|
||||
return self.m.discard_changes(*args, **kwargs).data_xml
|
||||
|
||||
Reference in New Issue
Block a user