IOS-XR NetConf and Cliconf plugin work (#33332)

*  - Netconf plugin addition for iosxr
 - Utilities refactoring to support netconf and cliconf
 - iosx_banner refactoring for netconf and cliconf
 - Integration testcases changes to accomodate above changes

* Fix sanity failures, shippable errors and review comments

* fix pep8 issue

* changes run_command method to send specific command args

* - Review comment fixes
- iosxr_command changes to remove ComplexDict based command_spec

* - Move namespaces removal method from utils to netconf plugin

* Minor refactoring in utils and change in deprecation message

* rewrite build_xml logic and import changes for new utils dir structure

* - Review comment changes and minor changes to documentation

* * refactor common code and docs updates
This commit is contained in:
Kedar Kekan 2017-12-06 22:37:31 +05:30 committed by GitHub
commit 2bc4c4f156
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1090 additions and 247 deletions

View file

@ -48,7 +48,17 @@ class ActionModule(_ActionModule):
elif self._play_context.connection == 'local':
provider = load_provider(iosxr_provider_spec, self._task.args)
pc = copy.deepcopy(self._play_context)
pc.connection = 'network_cli'
if self._task.action in ['iosxr_netconf', 'iosxr_config', 'iosxr_command'] or \
(provider['transport'] == 'cli' and (self._task.action == 'iosxr_banner' or
self._task.action == 'iosxr_facts' or self._task.action == 'iosxr_logging' or
self._task.action == 'iosxr_system' or self._task.action == 'iosxr_user' or
self._task.action == 'iosxr_interface')):
pc.connection = 'network_cli'
pc.port = int(provider['port'] or self._play_context.port or 22)
else:
pc.connection = 'netconf'
pc.port = int(provider['port'] or self._play_context.port or 830)
pc.network_os = 'iosxr'
pc.remote_addr = provider['host'] or self._play_context.remote_addr
pc.port = int(provider['port'] or self._play_context.port or 22)
@ -70,15 +80,16 @@ class ActionModule(_ActionModule):
# make sure we are in the right cli context which should be
# enable mode and not config module
if socket_path is None:
socket_path = self._connection.socket_path
if pc.connection == 'network_cli':
if socket_path is None:
socket_path = self._connection.socket_path
conn = Connection(socket_path)
out = conn.get_prompt()
while to_text(out, errors='surrogate_then_replace').strip().endswith(')#'):
display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr)
conn.send_command('abort')
conn = Connection(socket_path)
out = conn.get_prompt()
while to_text(out, errors='surrogate_then_replace').strip().endswith(')#'):
display.vvvv('wrong context, sending exit to device', self._play_context.remote_addr)
conn.send_command('abort')
out = conn.get_prompt()
result = super(ActionModule, self).run(tmp, task_vars)
return result

View file

@ -56,14 +56,19 @@ class Cliconf(CliconfBase):
return device_info
def get_config(self, source='running'):
def get_config(self, source='running', filter=None):
lookup = {'running': 'running-config'}
if source not in lookup:
return self.invalid_params("fetching configuration from %s is not supported" % source)
return self.send_command(to_bytes(b'show %s' % lookup[source], errors='surrogate_or_strict'))
if filter:
cmd = to_bytes(b'show {0} {1}'.format(lookup[source], filter), errors='surrogate_or_strict')
else:
cmd = to_bytes(b'show {0}'.format(lookup[source]), errors='surrogate_or_strict')
return self.send_command(cmd)
def edit_config(self, command):
for cmd in chain([b'configure'], to_list(command), [b'end']):
for cmd in chain(to_list(command)):
self.send_command(cmd)
def get(self, command, prompt=None, answer=None, sendonly=False):

View file

@ -54,10 +54,9 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
problems.
List of supported rpc's:
:get: Retrieves running configuration and device state information
:get_config: Retrieves the specified configuration from the device
:edit_config: Loads the specified commands into the remote device
:get: Execute specified command on remote device
:get_capabilities: Retrieves device information and supported rpc methods
:commit: Load configuration from candidate to running
:discard_changes: Discard changes to candidate datastore
:validate: Validate the contents of the specified configuration.
@ -65,6 +64,9 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:unlock: Release a configuration lock, previously obtained with the lock operation.
:copy_config: create or replace an entire configuration datastore with the contents of another complete
configuration datastore.
:get-schema: Retrieves the required schema from the device
:get_capabilities: Retrieves device information and supported rpc methods
For JUNOS:
:execute_rpc: RPC to be execute on remote device
:load_configuration: Loads given configuration on device
@ -100,7 +102,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:source: name of the configuration datastore being queried
:filter: specifies the portion of the configuration to retrieve
(by default entire configuration is retrieved)"""
return self.m.get_config(*args, **kwargs).data_xml
pass
@ensure_connected
def get(self, *args, **kwargs):
@ -108,7 +110,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
*filter* specifies the portion of the configuration to retrieve
(by default entire configuration is retrieved)
"""
return self.m.get(*args, **kwargs).data_xml
pass
@ensure_connected
def edit_config(self, *args, **kwargs):
@ -122,10 +124,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:error_option: if specified must be one of { `"stop-on-error"`, `"continue-on-error"`, `"rollback-on-error"` }
The `"rollback-on-error"` *error_option* depends on the `:rollback-on-error` capability.
"""
try:
return self.m.edit_config(*args, **kwargs).data_xml
except RPCError as exc:
raise Exception(to_xml(exc.xml))
pass
@ensure_connected
def validate(self, *args, **kwargs):
@ -133,7 +132,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:source: is the name of the configuration datastore being validated or `config`
element containing the configuration subtree to be validated
"""
return self.m.validate(*args, **kwargs).data_xml
pass
@ensure_connected
def copy_config(self, *args, **kwargs):
@ -162,7 +161,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
def discard_changes(self, *args, **kwargs):
"""Revert the candidate configuration to the currently running configuration.
Any uncommitted changes are discarded."""
return self.m.discard_changes(*args, **kwargs).data_xml
pass
@ensure_connected
def commit(self, *args, **kwargs):
@ -176,10 +175,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:confirmed: whether this is a confirmed commit
:timeout: specifies the confirm timeout in seconds
"""
try:
return self.m.commit(*args, **kwargs).data_xml
except RPCError as exc:
raise Exception(to_xml(exc.xml))
pass
@ensure_connected
def validate(self, *args, **kwargs):
@ -187,8 +183,18 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:source: name of configuration data store"""
return self.m.validate(*args, **kwargs).data_xml
@ensure_connected
def get_schema(self, *args, **kwargs):
"""Retrieves the required schema from the device
"""
return self.m.get_schema(*args, **kwargs)
@ensure_connected
def locked(self, *args, **kwargs):
return self.m.locked(*args, **kwargs)
@abstractmethod
def get_capabilities(self, commands):
def get_capabilities(self):
"""Retrieves device information and supported
rpc methods by device platform and return result
as a string
@ -213,3 +219,5 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
def fetch_file(self, source, destination):
"""Fetch file over scp from remote device"""
pass
# TODO Restore .data_xml, when ncclient supports it for all platforms

View file

@ -0,0 +1,209 @@
#
# (c) 2017 Red Hat Inc.
# (c) 2017 Kedar Kekan (kkekan@redhat.com)
#
# 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 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.iosxr.iosxr import build_xml
from ansible.errors import AnsibleConnectionFailure, AnsibleError
from ansible.plugins.netconf import NetconfBase
from ansible.plugins.netconf import ensure_connected
try:
from ncclient import manager
from ncclient.operations import RPCError
from ncclient.transport.errors import SSHUnknownHostError
from ncclient.xml_ import to_ele, to_xml, new_ele
except ImportError:
raise AnsibleError("ncclient is not installed")
try:
from lxml import etree
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:
print("utf8")
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
def get_device_info(self):
device_info = {}
device_info['network_os'] = 'iosxr'
install_meta = collections.OrderedDict()
install_meta.update([
('boot-variables', {'xpath': 'install/boot-variables', 'tag': True}),
('boot-variable', {'xpath': 'install/boot-variables/boot-variable', 'tag': True, 'lead': True}),
('software', {'xpath': 'install/software', 'tag': True}),
('alias-devices', {'xpath': 'install/software/alias-devices', 'tag': True}),
('alias-device', {'xpath': 'install/software/alias-devices/alias-device', 'tag': True}),
('m:device-name', {'xpath': 'install/software/alias-devices/alias-device/device-name', 'value': 'disk0:'}),
])
install_filter = build_xml('install', install_meta, opcode='filter')
reply = self.get(install_filter)
ele_boot_variable = etree.fromstring(reply).find('.//boot-variable/boot-variable')
if ele_boot_variable:
device_info['network_os_image'] = re.split('[:|,]', ele_boot_variable.text)[1]
ele_package_name = etree.fromstring(reply).find('.//package-name')
if ele_package_name:
device_info['network_os_package'] = ele_package_name.text
device_info['network_os_version'] = re.split('-', ele_package_name.text)[-1]
hostname_filter = build_xml('host-names', opcode='filter')
reply = self.get(hostname_filter)
device_info['network_os_hostname'] = etree.fromstring(reply).find('.//host-name').text
return device_info
def get_capabilities(self):
result = dict()
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes', 'validate', 'lock', 'unlock', 'get-schema']
result['network_api'] = 'netconf'
result['device_info'] = self.get_device_info()
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
return json.dumps(result)
@staticmethod
def guess_network_os(obj):
try:
m = manager.connect(
host=obj._play_context.remote_addr,
port=obj._play_context.port or 830,
username=obj._play_context.remote_user,
password=obj._play_context.password,
key_filename=str(obj.key_filename),
hostkey_verify=C.HOST_KEY_CHECKING,
look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS,
allow_agent=obj.allow_agent,
timeout=obj._play_context.timeout
)
except SSHUnknownHostError as exc:
raise AnsibleConnectionFailure(str(exc))
guessed_os = None
for c in m.server_capabilities:
if re.search('IOS-XR', c):
guessed_os = 'iosxr'
break
m.close_session()
return guessed_os
# TODO: change .xml to .data_xml, when ncclient supports data_xml on all platforms
@ensure_connected
def get(self, *args, **kwargs):
try:
response = self.m.get(*args, **kwargs)
return to_xml(remove_namespaces(response))
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected
def get_config(self, *args, **kwargs):
try:
response = self.m.get_config(*args, **kwargs)
return to_xml(remove_namespaces(response))
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected
def edit_config(self, *args, **kwargs):
try:
response = self.m.edit_config(*args, **kwargs)
return to_xml(remove_namespaces(response))
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected
def commit(self, *args, **kwargs):
try:
response = self.m.commit(*args, **kwargs)
return to_xml(remove_namespaces(response))
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected
def validate(self, *args, **kwargs):
try:
response = self.m.validate(*args, **kwargs)
return to_xml(remove_namespaces(response))
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected
def discard_changes(self, *args, **kwargs):
try:
response = self.m.discard_changes(*args, **kwargs)
return to_xml(remove_namespaces(response))
except RPCError as exc:
raise Exception(to_xml(exc.xml))

View file

@ -152,3 +152,39 @@ class Netconf(NetconfBase):
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