mirror of https://github.com/ansible/ansible.git
Add support for cliconf and netconf plugin (#25093)
* ansible-connection refactor and action plugin changes * Add cliconf plugin for eos, ios, iosxr, junos, nxos, vyos * Add netconf plugin for junos * Add jsonrpc support * Modify network_cli and netconf connection plugin * Fix py3 unit test failure * Fix review comment * Minor fixes * Fix ansible-connection review comments * Fix CI issue * platform_agnostic related changespull/12880/merge
parent
c20285782d
commit
6215922889
@ -0,0 +1,188 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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 signal
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from functools import wraps
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
||||
from ansible.module_utils.six import with_metaclass
|
||||
|
||||
try:
|
||||
from __main__ import display
|
||||
except ImportError:
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
|
||||
def enable_mode(func):
|
||||
@wraps(func)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
prompt = self.get_prompt()
|
||||
if not str(prompt).strip().endswith('#'):
|
||||
raise AnsibleError('operation requires privilege escalation')
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
class CliconfBase(with_metaclass(ABCMeta, object)):
|
||||
"""
|
||||
A base class for implementing cli connections
|
||||
|
||||
.. note:: Unlike most of Ansible, nearly all strings in
|
||||
:class:`CliconfBase` plugins are byte strings. This is because of
|
||||
how close to the underlying platform these plugins operate. Remember
|
||||
to mark literal strings as byte string (``b"string"``) and to use
|
||||
:func:`~ansible.module_utils._text.to_bytes` and
|
||||
:func:`~ansible.module_utils._text.to_text` to avoid unexpected
|
||||
problems.
|
||||
|
||||
List of supported rpc's:
|
||||
: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
|
||||
|
||||
Note: List of supported rpc's for remote device can be extracted from
|
||||
output of get_capabilities()
|
||||
|
||||
:returns: Returns output received from remote device as byte string
|
||||
|
||||
Usage:
|
||||
from ansible.module_utils.connection import Connection
|
||||
|
||||
conn = Connection()
|
||||
conn.get('show lldp neighbors detail'')
|
||||
conn.get_config('running')
|
||||
conn.edit_config(['hostname test', 'netconf ssh'])
|
||||
"""
|
||||
|
||||
def __init__(self, connection):
|
||||
self._connection = connection
|
||||
|
||||
def _alarm_handler(self, signum, frame):
|
||||
raise AnsibleConnectionFailure('timeout waiting for command to complete')
|
||||
|
||||
def send_command(self, command, prompt=None, answer=None, sendonly=False):
|
||||
"""Executes a cli command and returns the results
|
||||
This method will execute the CLI command on the connection and return
|
||||
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)
|
||||
resp = self._connection.send(command, prompt, answer, sendonly)
|
||||
signal.alarm(0)
|
||||
return resp
|
||||
|
||||
def get_prompt(self):
|
||||
"""Returns the current prompt from the device"""
|
||||
return self._connection._matched_prompt
|
||||
|
||||
def get_base_rpc(self):
|
||||
"""Returns list of base rpc method supported by remote device"""
|
||||
return ['get_config', 'edit_config', 'get_capabilities', 'get']
|
||||
|
||||
@abstractmethod
|
||||
def get_config(self, source='running', format='text'):
|
||||
"""Retrieves the specified configuration from the device
|
||||
This method will retrieve the configuration specified by source and
|
||||
return it to the caller as a string. Subsequent calls to this method
|
||||
will retrieve a new configuration from the device
|
||||
:args:
|
||||
arg[0] source: Datastore from which configuration should be retrieved eg: running/candidate/startup. (optional)
|
||||
default is running.
|
||||
arg[1] format: Output format in which configuration is retrieved
|
||||
Note: Specified datastore should be supported by remote device.
|
||||
:kwargs:
|
||||
Keywords supported
|
||||
:command: the command string to execute
|
||||
:source: Datastore from which configuration should be retrieved
|
||||
:format: Output format in which configuration is retrieved
|
||||
:returns: Returns output received from remote device as byte string
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def edit_config(self, commands):
|
||||
"""Loads the specified commands into the remote device
|
||||
This method will load the commands into the remote device. This
|
||||
method will make sure the device is in the proper context before
|
||||
send the commands (eg config mode)
|
||||
:args:
|
||||
arg[0] command: List of configuration commands
|
||||
:kwargs:
|
||||
Keywords supported
|
||||
:command: the command string to execute
|
||||
:returns: Returns output received from remote device as byte string
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self, *args, **kwargs):
|
||||
"""Execute specified command on remote device
|
||||
This method will retrieve the specified data and
|
||||
return it to the caller as a string.
|
||||
:args:
|
||||
arg[0] command: command in string format to be executed on remote device
|
||||
arg[1] prompt: the expected prompt generated by executing command.
|
||||
This can be a string or a list of strings (optional)
|
||||
arg[2] answer: the string to respond to the prompt with (optional)
|
||||
arg[3] sendonly: bool to disable waiting for response, default is false (optional)
|
||||
:kwargs:
|
||||
:command: the command string to execute
|
||||
:prompt: the expected prompt generated by executing command.
|
||||
This can be a string or a list of strings
|
||||
:answer: the string to respond to the prompt with
|
||||
:sendonly: bool to disable waiting for response
|
||||
:returns: Returns output received from remote device as byte string
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_capabilities(self):
|
||||
"""Retrieves device information and supported
|
||||
rpc methods by device platform and return result
|
||||
as a string
|
||||
:returns: Returns output received from remote device as byte string
|
||||
"""
|
||||
pass
|
||||
|
||||
def commit(self, comment=None):
|
||||
"""Commit configuration changes"""
|
||||
return self._connection.method_not_found("commit is not supported by network_os %s" % self._play_context.network_os)
|
||||
|
||||
def discard_changes(self):
|
||||
"Discard changes in candidate datastore"
|
||||
return self._connection.method_not_found("discard_changes is not supported by network_os %s" % self._play_context.network_os)
|
||||
|
||||
def put_file(self, source, destination):
|
||||
"""Copies file over scp to remote device"""
|
||||
pass
|
||||
|
||||
def fetch_file(self, source, destination):
|
||||
"""Fetch file over scp from remote device"""
|
||||
pass
|
@ -0,0 +1,73 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils.network_common import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'eos'
|
||||
reply = self.get(b'show version | json')
|
||||
data = json.loads(reply)
|
||||
|
||||
device_info['network_os_version'] = data['version']
|
||||
device_info['network_os_model'] = data['modelName']
|
||||
|
||||
reply = self.get(b'show hostname | json')
|
||||
data = json.loads(reply)
|
||||
|
||||
device_info['network_os_hostname'] = data['hostname']
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running', format='text'):
|
||||
lookup = {'running': 'running-config', 'startup': 'startup-config'}
|
||||
if source not in lookup:
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
if format == 'text':
|
||||
cmd = b'show %s' % lookup[source]
|
||||
else:
|
||||
cmd = b'show %s | %s' % (lookup[source], format)
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain([b'configure'], to_list(command), [b'end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.send_command(*args, **kwargs)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc()
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
return json.dumps(result)
|
@ -0,0 +1,78 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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 re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.module_utils.network_common import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'ios'
|
||||
reply = self.get(b'show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Version (\S+),', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'^Cisco (.+) \(revision', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'^(.+) uptime', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
@enable_mode
|
||||
def get_config(self, source='running'):
|
||||
if source not in ('running', 'startup'):
|
||||
return self.invalid_params("fetching configuration from %s is not supported" % source)
|
||||
if source == 'running':
|
||||
cmd = b'show running-config all'
|
||||
else:
|
||||
cmd = b'show startup-config'
|
||||
return self.send_command(cmd)
|
||||
|
||||
@enable_mode
|
||||
def edit_config(self, command):
|
||||
for cmd in chain([b'configure terminal'], to_list(command), [b'end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.send_command(*args, **kwargs)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc()
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
return json.dumps(result)
|
@ -0,0 +1,87 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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 re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.module_utils.network_common import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'iosxr'
|
||||
reply = self.get(b'show version brief')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Version (\S+)$', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'image file is "(.+)"', data)
|
||||
if match:
|
||||
device_info['network_os_image'] = match.group(1)
|
||||
|
||||
match = re.search(r'^Cisco (.+) \(revision', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
match = re.search(r'^(.+) uptime', data, re.M)
|
||||
if match:
|
||||
device_info['network_os_hostname'] = match.group(1)
|
||||
|
||||
return device_info
|
||||
|
||||
def get_config(self, source='running'):
|
||||
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'))
|
||||
|
||||
def edit_config(self, command):
|
||||
for cmd in chain([b'configure'], to_list(command), [b'end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.send_command(*args, **kwargs)
|
||||
|
||||
def commit(self, comment=None):
|
||||
if comment:
|
||||
command = b'commit comment {0}'.format(comment)
|
||||
else:
|
||||
command = b'commit'
|
||||
self.send_command(command)
|
||||
|
||||
def discard_changes(self):
|
||||
self.send_command(b'abort')
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes']
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
return json.dumps(result)
|
@ -0,0 +1,87 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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 re
|
||||
import json
|
||||
|
||||
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
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_text(self, ele, tag):
|
||||
try:
|
||||
return to_text(ele.find(tag).text, errors='surrogate_then_replace').strip()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
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')
|
||||
|
||||
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')
|
||||
|
||||
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'
|
||||
else:
|
||||
cmd = b'show configuration | display %s' % format
|
||||
return self.send_command(to_bytes(cmd), errors='surrogate_or_strict')
|
||||
|
||||
def edit_config(self, command):
|
||||
for cmd in chain([b'configure'], to_list(command)):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.send_command(*args, **kwargs)
|
||||
|
||||
def commit(self, comment=None):
|
||||
if comment:
|
||||
command = b'commit comment {0}'.format(comment)
|
||||
else:
|
||||
command = b'commit'
|
||||
self.send_command(command)
|
||||
|
||||
def discard_changes(self):
|
||||
self.send_command(b'rollback')
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes']
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
return json.dumps(result)
|
@ -0,0 +1,62 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils.network_common import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'nxos'
|
||||
reply = self.get(b'show version | json')
|
||||
data = json.loads(reply)
|
||||
|
||||
device_info['network_os_version'] = data['sys_ver_str']
|
||||
device_info['network_os_model'] = data['chassis_id']
|
||||
device_info['network_os_hostname'] = data['host_name']
|
||||
device_info['network_os_image'] = data['isan_file_name']
|
||||
|
||||
return device_info
|
||||
|
||||
def get_config(self, source='running'):
|
||||
lookup = {'running': 'running-config', 'startup': 'startup-config'}
|
||||
return self.send_command(b'show %s' % lookup[source])
|
||||
|
||||
def edit_config(self, command):
|
||||
for cmd in chain([b'configure'], to_list(command), [b'end']):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.send_command(*args, **kwargs)
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc()
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
return json.dumps(result)
|
@ -0,0 +1,79 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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 re
|
||||
import json
|
||||
|
||||
from itertools import chain
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.module_utils.network_common import to_list
|
||||
from ansible.plugins.cliconf import CliconfBase, enable_mode
|
||||
|
||||
|
||||
class Cliconf(CliconfBase):
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'vyos'
|
||||
reply = self.get(b'show version')
|
||||
data = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
match = re.search(r'Version:\s*(\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_version'] = match.group(1)
|
||||
|
||||
match = re.search(r'HW model:\s*(\S+)', data)
|
||||
if match:
|
||||
device_info['network_os_model'] = match.group(1)
|
||||
|
||||
reply = self.get(b'show host name')
|
||||
device_info['network_os_hostname'] = to_text(reply, errors='surrogate_or_strict').strip()
|
||||
|
||||
return device_info
|
||||
|
||||
def get_config(self):
|
||||
return self.send_command(b'show configuration all')
|
||||
|
||||
def edit_config(self, command):
|
||||
for cmd in chain([b'configure'], to_list(command)):
|
||||
self.send_command(cmd)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return self.send_command(*args, **kwargs)
|
||||
|
||||
def commit(self, comment=None):
|
||||
if comment:
|
||||
command = b'commit comment {0}'.format(comment)
|
||||
else:
|
||||
command = b'commit'
|
||||
self.send_command(command)
|
||||
|
||||
def discard_changes(self, *args, **kwargs):
|
||||
self.send_command(b'discard')
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes']
|
||||
result['network_api'] = 'cliconf'
|
||||
result['device_info'] = self.get_device_info()
|
||||
return json.dumps(result)
|
@ -0,0 +1,189 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from functools import wraps
|
||||
|
||||
from ansible.module_utils.six import with_metaclass
|
||||
|
||||
|
||||
def ensure_connected(func):
|
||||
@wraps(func)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
if not self._connection._connected:
|
||||
self._connection._connect()
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
class NetconfBase(with_metaclass(ABCMeta, object)):
|
||||
"""
|
||||
A base class for implementing Netconf connections
|
||||
|
||||
.. note:: Unlike most of Ansible, nearly all strings in
|
||||
:class:`TerminalBase` plugins are byte strings. This is because of
|
||||
how close to the underlying platform these plugins operate. Remember
|
||||
to mark literal strings as byte string (``b"string"``) and to use
|
||||
:func:`~ansible.module_utils._text.to_bytes` and
|
||||
:func:`~ansible.module_utils._text.to_text` to avoid unexpected
|
||||
problems.
|
||||
|
||||
List of supported rpc's:
|
||||
: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.
|
||||
:lock: Allows the client to lock the configuration system of a device.
|
||||
: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.
|
||||
For JUNOS:
|
||||
:execute_rpc: RPC to be execute on remote device
|
||||
:load_configuration: Loads given configuration on device
|
||||
|
||||
Note: rpc support depends on the capabilites of remote device.
|
||||
|
||||
:returns: Returns output received from remote device as byte string
|
||||
Note: the 'result' or 'error' from response should to be converted to object
|
||||
of ElementTree using 'fromstring' to parse output as xml doc
|
||||
|
||||
'get_capabilities()' returns 'result' as a json string.
|
||||
|
||||
Usage:
|
||||
from ansible.module_utils.connection import Connection
|
||||
|
||||
conn = Connection()
|
||||
data = conn.execute_rpc(rpc)
|
||||
reply = fromstring(reply)
|
||||
|
||||
data = conn.get_capabilities()
|
||||
json.loads(data)
|
||||
|
||||
conn.load_configuration(config=[''set system ntp server 1.1.1.1''], action='set', format='text')
|
||||
"""
|
||||
|
||||
def __init__(self, connection):
|
||||
self._connection = connection
|
||||
self.m = self._connection._manager
|
||||
|
||||
@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)"""
|
||||
return self.m.get_config(*args, **kwargs).data_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)
|
||||
"""
|
||||
return self.m.get(*args, **kwargs).data_xml
|
||||
|
||||
@ensure_connected
|
||||
def edit_config(self, *args, **kwargs):
|
||||
"""Loads all or part of the specified *config* to the *target* configuration datastore.
|
||||
|
||||
:target: is the name of the configuration datastore being edited
|
||||
:config: is the configuration, which must be rooted in the `config` element.
|
||||
It can be specified either as a string or an :class:`~xml.etree.ElementTree.Element`.
|
||||
:default_operation: if specified must be one of { `"merge"`, `"replace"`, or `"none"` }
|
||||
:test_option: if specified must be one of { `"test_then_set"`, `"set"` }
|
||||
: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
|
||||
|
||||
@ensure_connected
|
||||
def validate(self, *args, **kwargs):
|
||||
"""Validate the contents of the specified configuration.
|
||||
: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
|
||||
|
||||
@ensure_connected
|
||||
def copy_config(self, *args, **kwargs):
|
||||
"""Create or replace an entire configuration datastore with the contents of another complete
|
||||
configuration datastore.
|
||||
:source: is the name of the configuration datastore to use as the source of the
|
||||
copy operation or `config` element containing the configuration subtree to copy
|
||||
:target: is the name of the configuration datastore to use as the destination of the copy operation"""
|
||||
return self.m.copy_config(*args, **kwargs).data_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
|
||||
"""
|
||||
return self.m.lock(*args, **kwargs).data_xml
|
||||
|
||||
@ensure_connected
|
||||
def unlock(self, *args, **kwargs):
|
||||
"""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
|
||||
|
||||
@ensure_connected
|
||||
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
|
||||
|
||||
@ensure_connected
|
||||
def commit(self, *args, **kwargs):
|
||||
"""Commit the candidate configuration as the device's new current configuration.
|
||||
Depends on the `:candidate` capability.
|
||||
A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no
|
||||
followup commit within the *timeout* interval. If no timeout is specified the
|
||||
confirm timeout defaults to 600 seconds (10 minutes).
|
||||
A confirming commit may have the *confirmed* parameter but this is not required.
|
||||
Depends on the `:confirmed-commit` capability.
|
||||
:confirmed: whether this is a confirmed commit
|
||||
:timeout: specifies the confirm timeout in seconds
|
||||
"""
|
||||
return self.m.commit(*args, **kwargs).data_xml
|
||||
|
||||
@abstractmethod
|
||||
def get_capabilities(self, commands):
|
||||
"""Retrieves device information and supported
|
||||
rpc methods by device platform and return result
|
||||
as a string
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_base_rpc(self):
|
||||
"""Returns list of base rpc method supported by remote device"""
|
||||
return ['get_config', 'edit_config', 'get_capabilities', 'get']
|
||||
|
||||
def put_file(self, source, destination):
|
||||
"""Copies file over scp to remote device"""
|
||||
pass
|
||||
|
||||
def fetch_file(self, source, destination):
|
||||
"""Fetch file over scp from remote device"""
|
||||
pass
|
@ -0,0 +1,79 @@
|
||||
#
|
||||
# (c) 2017 Red Hat Inc.
|
||||
#
|
||||
# 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
|
||||
|
||||
from xml.etree.ElementTree import fromstring
|
||||
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.plugins.netconf import NetconfBase
|
||||
from ansible.plugins.netconf import ensure_connected
|
||||
|
||||
from ncclient.xml_ import new_ele
|
||||
|
||||
|
||||
class Netconf(NetconfBase):
|
||||
|
||||
def get_text(self, ele, tag):
|
||||
try:
|
||||
return to_text(ele.find(tag).text, errors='surrogate_then_replace').strip()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def get_device_info(self):
|
||||
device_info = {}
|
||||
|
||||
device_info['network_os'] = 'junos'
|
||||
data = self.execute_rpc('get-software-information')
|
||||
reply = fromstring(data)
|
||||
sw_info = reply.find('.//software-information')
|
||||
|
||||
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')
|
||||
|
||||
return device_info
|
||||
|
||||
@ensure_connected
|
||||
def execute_rpc(self, rpc):
|
||||
"""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
|
||||
|
||||
@ensure_connected
|
||||
def load_configuration(self, *args, **kwargs):
|
||||
"""Loads given configuration on device
|
||||
:format: Format of configuration (xml, text, set)
|
||||
: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
|
||||
|
||||
def get_capabilities(self):
|
||||
result = {}
|
||||
result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes', 'validate', 'lock', 'unlock', 'copy_copy']
|
||||
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)
|
@ -0,0 +1,115 @@
|
||||
#
|
||||
# (c) 2016 Red Hat Inc.
|
||||
#
|
||||
# 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 traceback
|
||||
|
||||
from ansible.module_utils._text import to_text
|
||||
|
||||
try:
|
||||
from __main__ import display
|
||||
except ImportError:
|
||||
from ansible.utils.display import Display
|
||||
display = Display()
|
||||
|
||||
|
||||
class Rpc:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._rpc = set()
|
||||
super(Rpc, self).__init__(*args, **kwargs)
|
||||
|
||||
def _exec_rpc(self, request):
|
||||
method = request.get('method')
|
||||
|
||||
if method.startswith('rpc.') or method.startswith('_'):
|
||||
error = self.invalid_request()
|
||||
return json.dumps(error)
|
||||
|
||||
params = request.get('params')
|
||||
setattr(self, '_identifier', request.get('id'))
|
||||
args = []
|
||||
kwargs = {}
|
||||
|
||||
if all((params, isinstance(params, list))):
|
||||
args = params
|
||||
elif all((params, isinstance(params, dict))):
|
||||
kwargs = params
|
||||
|
||||
rpc_method = None
|
||||
for obj in self._rpc:
|
||||
rpc_method = getattr(obj, method, None)
|
||||
if rpc_method:
|
||||
break
|
||||
|
||||
if not rpc_method:
|
||||
error = self.method_not_found()
|
||||
response = json.dumps(error)
|
||||
else:
|
||||
try:
|
||||
result = rpc_method(*args, **kwargs)
|
||||
display.display(" -- result -- %s" % result, log_only=True)
|
||||
except Exception as exc:
|
||||
display.display(traceback.format_exc(), log_only=True)
|
||||
error = self.internal_error(data=to_text(exc, errors='surrogate_then_replace'))
|
||||
response = json.dumps(error)
|
||||
else:
|
||||
if isinstance(result, dict) and 'jsonrpc' in result:
|
||||
response = result
|
||||
else:
|
||||
response = self.response(result)
|
||||
|
||||
response = json.dumps(response)
|
||||
|
||||
display.display(" -- response -- %s" % response, log_only=True)
|
||||
delattr(self, '_identifier')
|
||||
return response
|
||||
|
||||
def header(self):
|
||||
return {'jsonrpc': '2.0', 'id': self._identifier}
|
||||
|
||||
def response(self, result=None):
|
||||
response = self.header()
|
||||
response['result'] = result or 'ok'
|
||||
return response
|
||||
|
||||
def error(self, code, message, data=None):
|
||||
response = self.header()
|
||||
error = {'code': code, 'message': message}
|
||||
if data:
|
||||
error['data'] = data
|
||||
response['error'] = error
|
||||
return response
|
||||
|
||||
# json-rpc standard errors (-32768 .. -32000)
|
||||
def parse_error(self, data=None):
|
||||
return self.error(-32700, 'Parse error', data)
|
||||
|
||||
def method_not_found(self, data=None):
|
||||
return self.error(-32601, 'Method not found', data)
|
||||
|
||||
def invalid_request(self, data=None):
|
||||
return self.error(-32600, 'Invalid request', data)
|
||||
|
||||
def invalid_params(self, data=None):
|
||||
return self.error(-32602, 'Invalid params', data)
|
||||
|
||||
def internal_error(self, data=None):
|
||||
return self.error(-32603, 'Internal error', data)
|
Loading…
Reference in New Issue