Refactor junos modules to Use netconf and cliconf plugins (#32621)

* Fix junos integration test fixes as per connection refactor (#33050)

Refactor netconf connection plugin to work with netconf plugin

* Fix junos integration test fixes as per connection refactor (#33050)

Refactor netconf connection plugin to work with netconf plugin
Fix CI failure
Fix unit test failure
Fix review comments
This commit is contained in:
Ganesh Nalawade 2017-11-24 12:04:47 +05:30 committed by GitHub
commit 3d63ecb6f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 543 additions and 320 deletions

View file

@ -87,7 +87,7 @@ class ActionModule(_ActionModule):
conn = Connection(socket_path)
out = conn.get_prompt()
while to_text(out, errors='surrogate_then_replace').strip().endswith(')#'):
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('exit')
out = conn.get_prompt()

View file

@ -89,7 +89,9 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
self._connection = connection
def _alarm_handler(self, signum, frame):
raise AnsibleConnectionFailure('timeout waiting for command to complete')
"""Alarm handler raised in case of command timeout """
display.display('closing shell due to command timeout (%s seconds).' % self._connection._play_context.timeout, log_only=True)
self.close()
def send_command(self, command, prompt=None, answer=None, sendonly=False):
"""Executes a cli command and returns the results
@ -97,10 +99,9 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
the results to the caller. The command output will be returned as a
string
"""
timeout = self._connection._play_context.timeout or 30
signal.signal(signal.SIGALRM, self._alarm_handler)
signal.alarm(timeout)
display.display("command: %s" % command, log_only=True)
if not signal.getsignal(signal.SIGALRM):
signal.signal(signal.SIGALRM, self._alarm_handler)
signal.alarm(self._connection._play_context.timeout)
resp = self._connection.send(command, prompt, answer, sendonly)
signal.alarm(0)
return resp

View file

@ -19,11 +19,9 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import json
import re
from itertools import chain
from xml.etree.ElementTree import fromstring
from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.network_common import to_list
@ -39,49 +37,66 @@ class Cliconf(CliconfBase):
pass
def get_device_info(self):
device_info = {}
device_info = dict()
device_info['network_os'] = 'junos'
reply = self.get(b'show version | display xml')
data = fromstring(to_text(reply, errors='surrogate_then_replace').strip())
sw_info = data.find('.//software-information')
reply = self.get(command='show version')
data = to_text(reply, errors='surrogate_or_strict').strip()
device_info['network_os_version'] = self.get_text(sw_info, 'junos-version')
device_info['network_os_hostname'] = self.get_text(sw_info, 'host-name')
device_info['network_os_model'] = self.get_text(sw_info, 'product-model')
match = re.search(r'Junos: (\S+)', data)
if match:
device_info['network_os_version'] = match.group(1)
match = re.search(r'Model: (\S+)', data, re.M)
if match:
device_info['network_os_model'] = match.group(1)
match = re.search(r'Hostname: (\S+)', data, re.M)
if match:
device_info['network_os_hostname'] = match.group(1)
return device_info
def get_config(self, source='running', format='text'):
if source != 'running':
return self.invalid_params("fetching configuration from %s is not supported" % source)
if format == 'text':
cmd = b'show configuration'
cmd = 'show configuration'
else:
cmd = b'show configuration | display %s' % format
return self.send_command(to_bytes(cmd, errors='surrogate_or_strict'))
cmd = 'show configuration | display %s' % format
return self.send_command(cmd)
def edit_config(self, command):
for cmd in chain([b'configure'], to_list(command)):
for cmd in chain(['configure'], to_list(command)):
self.send_command(cmd)
def get(self, *args, **kwargs):
return self.send_command(*args, **kwargs)
command = kwargs.get('command')
return self.send_command(command)
def commit(self, comment=None):
def commit(self, *args, **kwargs):
comment = kwargs.get('comment', None)
command = b'commit'
if comment:
command = b'commit comment {0}'.format(comment)
else:
command = b'commit'
self.send_command(command)
command += b' comment {0}'.format(comment)
command += b' and-quit'
return self.send_command(command)
def discard_changes(self):
self.send_command(b'rollback')
def discard_changes(self, rollback_id=None):
command = b'rollback'
if rollback_id is not None:
command += b' %s' % int(rollback_id)
for cmd in chain(to_list(command), b'exit'):
self.send_command(cmd)
def get_capabilities(self):
result = {}
result = dict()
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes']
result['network_api'] = 'cliconf'
result['device_info'] = self.get_device_info()
return json.dumps(result)
def compare_configuration(self, rollback_id=None):
command = b'show | compare'
if rollback_id is not None:
command += b' rollback %s' % int(rollback_id)
return self.send_command(command)

View file

@ -71,10 +71,11 @@ DOCUMENTATION = """
import os
import logging
import json
from ansible import constants as C
from ansible.errors import AnsibleConnectionFailure, AnsibleError
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
from ansible.plugins.loader import netconf_loader
from ansible.plugins.connection import ConnectionBase, ensure_connect
@ -110,11 +111,20 @@ class Connection(ConnectionBase):
self._network_os = self._play_context.network_os or 'default'
display.display('network_os is set to %s' % self._network_os, log_only=True)
self._netconf = None
self._manager = None
self._connected = False
self._local = LocalConnection(play_context, new_stdin, *args, **kwargs)
def __getattr__(self, name):
try:
return self.__dict__[name]
except KeyError:
if name.startswith('_'):
raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))
return getattr(self._netconf, name)
def exec_command(self, request, in_data=None, sudoable=True):
"""Sends the request to the node and returns the reply
The method accepts two forms of request. The first form is as a byte
@ -131,7 +141,8 @@ class Connection(ConnectionBase):
try:
reply = self._manager.rpc(request)
except RPCError as exc:
return to_xml(exc.xml)
error = self.internal_error(data=to_text(to_xml(exc.xml), errors='surrogate_or_strict'))
return json.dumps(error)
return reply.data_xml
else:

View file

@ -22,8 +22,15 @@ __metaclass__ = type
from abc import ABCMeta, abstractmethod
from functools import wraps
from ansible.errors import AnsibleError
from ansible.module_utils.six import with_metaclass
try:
from ncclient.operations import RPCError
from ncclient.xml_ import to_xml
except ImportError:
raise AnsibleError("ncclient is not installed")
def ensure_connected(func):
@wraps(func)
@ -115,7 +122,10 @@ 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.
"""
return self.m.get_config(*args, **kwargs).data_xml
try:
return self.m.edit_config(*args, **kwargs).data_xml
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected
def validate(self, *args, **kwargs):
@ -146,7 +156,7 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
"""Release a configuration lock, previously obtained with the lock operation.
:target: is the name of the configuration datastore to unlock
"""
return self.m.lock(*args, **kwargs).data_xml
return self.m.unlock(*args, **kwargs).data_xml
@ensure_connected
def discard_changes(self, *args, **kwargs):
@ -166,7 +176,16 @@ class NetconfBase(with_metaclass(ABCMeta, object)):
:confirmed: whether this is a confirmed commit
:timeout: specifies the confirm timeout in seconds
"""
return self.m.commit(*args, **kwargs).data_xml
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):
"""Validate the contents of the specified configuration.
:source: name of configuration data store"""
return self.m.validate(*args, **kwargs).data_xml
@abstractmethod
def get_capabilities(self, commands):

View file

@ -22,10 +22,8 @@ __metaclass__ = type
import json
import re
from xml.etree.ElementTree import fromstring
from ansible import constants as C
from ansible.module_utils._text import to_text
from ansible.module_utils._text import to_text, to_bytes
from ansible.errors import AnsibleConnectionFailure, AnsibleError
from ansible.plugins.netconf import NetconfBase
from ansible.plugins.netconf import ensure_connected
@ -48,11 +46,11 @@ class Netconf(NetconfBase):
pass
def get_device_info(self):
device_info = {}
device_info = dict()
device_info['network_os'] = 'junos'
data = self.execute_rpc('get-software-information')
reply = fromstring(data)
ele = new_ele('get-software-information')
data = self.execute_rpc(to_xml(ele))
reply = to_ele(to_bytes(data, errors='surrogate_or_strict'))
sw_info = reply.find('.//software-information')
device_info['network_os_version'] = self.get_text(sw_info, 'junos-version')
@ -62,11 +60,14 @@ class Netconf(NetconfBase):
return device_info
@ensure_connected
def execute_rpc(self, rpc):
def execute_rpc(self, name):
"""RPC to be execute on remote device
:rpc: Name of rpc in string format"""
name = new_ele(rpc)
return self.m.rpc(name).data_xml
:name: Name of rpc in string format"""
try:
obj = to_ele(to_bytes(name, errors='surrogate_or_strict'))
return self.m.rpc(obj).data_xml
except RPCError as exc:
raise Exception(to_xml(exc.xml))
@ensure_connected
def load_configuration(self, *args, **kwargs):
@ -75,11 +76,21 @@ class Netconf(NetconfBase):
:action: Action to be performed (merge, replace, override, update)
:target: is the name of the configuration datastore being edited
:config: is the configuration in string format."""
return self.m.load_configuration(*args, **kwargs).data_xml
if kwargs.get('config'):
kwargs['config'] = to_bytes(kwargs['config'], errors='surrogate_or_strict')
if kwargs.get('format', 'xml') == 'xml':
kwargs['config'] = to_ele(kwargs['config'])
try:
return self.m.load_configuration(*args, **kwargs).data_xml
except RPCError as exc:
raise Exception(to_xml(exc.xml))
def get_capabilities(self):
result = {}
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes', 'validate', 'lock', 'unlock', 'copy_copy']
result = dict()
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes', 'validate', 'lock', 'unlock', 'copy_copy',
'execute_rpc', 'load_configuration', 'get_configuration', 'command',
'reboot', 'halt']
result['network_api'] = 'netconf'
result['device_info'] = self.get_device_info()
result['server_capabilities'] = [c for c in self.m.server_capabilities]
@ -112,3 +123,32 @@ class Netconf(NetconfBase):
m.close_session()
return guessed_os
@ensure_connected
def get_configuration(self, *args, **kwargs):
"""Retrieve all or part of a specified configuration.
:format: format in configuration should be retrieved
:filter: specifies the portion of the configuration to retrieve
(by default entire configuration is retrieved)"""
return self.m.get_configuration(*args, **kwargs).data_xml
@ensure_connected
def compare_configuration(self, *args, **kwargs):
"""Compare configuration
:rollback: rollback id"""
return self.m.compare_configuration(*args, **kwargs).data_xml
@ensure_connected
def halt(self):
"""reboot the device"""
return self.m.halt().data_xml
@ensure_connected
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