mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-22 12:50:22 -07:00
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:
parent
0c75f00248
commit
3d63ecb6f3
37 changed files with 543 additions and 320 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue