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:
Ganesh Nalawade
2018-05-17 17:38:12 +05:30
committed by GitHub
parent 4c0ceaea3d
commit 30f992f260
16 changed files with 841 additions and 119 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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