2016-11-28 17:49:40 +00:00
|
|
|
# (c) 2016 Red Hat Inc.
|
2017-08-20 15:20:30 +00:00
|
|
|
# (c) 2017 Ansible Project
|
|
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
|
2017-09-08 18:08:31 +00:00
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
|
|
|
|
|
|
|
DOCUMENTATION = """
|
2017-08-20 15:20:30 +00:00
|
|
|
author: Ansible Networking Team
|
|
|
|
connection: network_cli
|
|
|
|
short_description: Use network_cli to run command on network appliances
|
|
|
|
description:
|
|
|
|
- This plugin actually forces use of 'local' execution but using paramiko to establish a remote ssh shell on the appliance.
|
|
|
|
- Also this plugin ignores the become_method but still uses the becoe_user and become_pass to
|
|
|
|
do privilege escalation, method depending on network_os used.
|
|
|
|
version_added: "2.3"
|
|
|
|
options:
|
|
|
|
network_os:
|
|
|
|
description:
|
|
|
|
- Appliance specific OS
|
|
|
|
default: 'default'
|
|
|
|
vars:
|
|
|
|
- name: ansible_netconf_network_os
|
|
|
|
password:
|
|
|
|
description:
|
|
|
|
- Secret used to authenticate
|
|
|
|
vars:
|
|
|
|
- name: ansible_pass
|
|
|
|
- name: ansible_netconf_pass
|
|
|
|
private_key_file:
|
|
|
|
description:
|
|
|
|
- Key or certificate file used for authentication
|
2017-09-10 21:53:49 +00:00
|
|
|
ini:
|
|
|
|
- section: defaults
|
|
|
|
key: private_key_file
|
|
|
|
env:
|
|
|
|
- name: ANSIBLE_PRIVATE_KEY_FILE
|
2017-08-20 15:20:30 +00:00
|
|
|
vars:
|
|
|
|
- name: ansible_private_key_file
|
|
|
|
timeout:
|
|
|
|
type: int
|
|
|
|
description:
|
|
|
|
- Connection timeout in seconds
|
|
|
|
default: 120
|
|
|
|
"""
|
|
|
|
|
2016-11-28 17:49:40 +00:00
|
|
|
import json
|
2017-05-12 16:13:51 +00:00
|
|
|
import logging
|
|
|
|
import re
|
2017-11-09 20:04:40 +00:00
|
|
|
import os
|
2017-05-12 16:13:51 +00:00
|
|
|
import socket
|
2017-02-20 20:37:14 +00:00
|
|
|
import traceback
|
2017-06-06 08:26:25 +00:00
|
|
|
|
2017-03-29 19:12:18 +00:00
|
|
|
from ansible import constants as C
|
2016-11-28 17:49:40 +00:00
|
|
|
from ansible.errors import AnsibleConnectionFailure
|
2017-11-16 18:49:57 +00:00
|
|
|
from ansible.module_utils.six import BytesIO, PY3
|
2017-11-14 20:51:14 +00:00
|
|
|
from ansible.module_utils.six.moves import cPickle
|
2017-05-12 16:13:51 +00:00
|
|
|
from ansible.module_utils._text import to_bytes, to_text
|
2017-11-14 20:51:14 +00:00
|
|
|
from ansible.playbook.play_context import PlayContext
|
2017-11-09 20:04:40 +00:00
|
|
|
from ansible.plugins.loader import cliconf_loader, terminal_loader, connection_loader
|
|
|
|
from ansible.plugins.connection import ConnectionBase
|
2017-11-16 18:49:57 +00:00
|
|
|
from ansible.utils.path import unfrackpath
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-03-21 03:08:02 +00:00
|
|
|
try:
|
|
|
|
from __main__ import display
|
|
|
|
except ImportError:
|
|
|
|
from ansible.utils.display import Display
|
|
|
|
display = Display()
|
|
|
|
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
class Connection(ConnectionBase):
|
2016-12-06 02:42:09 +00:00
|
|
|
''' CLI (shell) SSH connections on Paramiko '''
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
transport = 'network_cli'
|
2017-02-17 15:00:23 +00:00
|
|
|
has_pipelining = True
|
2017-11-09 20:04:40 +00:00
|
|
|
force_persistence = True
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
|
|
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
self._ssh_shell = None
|
|
|
|
|
2016-11-28 17:49:40 +00:00
|
|
|
self._matched_prompt = None
|
|
|
|
self._matched_pattern = None
|
|
|
|
self._last_response = None
|
|
|
|
self._history = list()
|
|
|
|
|
2017-11-16 18:49:57 +00:00
|
|
|
self._local = connection_loader.get('local', play_context, '/dev/null')
|
|
|
|
self._local.set_options()
|
2017-11-09 20:04:40 +00:00
|
|
|
|
|
|
|
self._terminal = None
|
|
|
|
self._cliconf = None
|
|
|
|
|
|
|
|
if self._play_context.verbosity > 3:
|
2017-03-21 02:26:18 +00:00
|
|
|
logging.getLogger('paramiko').setLevel(logging.DEBUG)
|
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
# reconstruct the socket_path and set instance values accordingly
|
|
|
|
self._update_connection_state()
|
|
|
|
|
|
|
|
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._cliconf, name)
|
|
|
|
|
|
|
|
def exec_command(self, cmd, in_data=None, sudoable=True):
|
|
|
|
# this try..except block is just to handle the transition to supporting
|
|
|
|
# network_cli as a toplevel connection. Once connection=local is gone,
|
|
|
|
# this block can be removed as well and all calls passed directly to
|
|
|
|
# the local connection
|
|
|
|
if self._ssh_shell:
|
|
|
|
try:
|
|
|
|
cmd = json.loads(to_text(cmd, errors='surrogate_or_strict'))
|
|
|
|
kwargs = {'command': to_bytes(cmd['command'], errors='surrogate_or_strict')}
|
2017-11-15 03:36:28 +00:00
|
|
|
for key in ('prompt', 'answer', 'send_only'):
|
2017-11-17 16:33:54 +00:00
|
|
|
if cmd.get(key) is not None:
|
2017-11-09 20:04:40 +00:00
|
|
|
kwargs[key] = to_bytes(cmd[key], errors='surrogate_or_strict')
|
|
|
|
return self.send(**kwargs)
|
|
|
|
except ValueError:
|
|
|
|
cmd = to_bytes(cmd, errors='surrogate_or_strict')
|
|
|
|
return self.send(command=cmd)
|
|
|
|
|
|
|
|
else:
|
|
|
|
return self._local.exec_command(cmd, in_data, sudoable)
|
|
|
|
|
|
|
|
def put_file(self, in_path, out_path):
|
|
|
|
return self._local.put_file(in_path, out_path)
|
|
|
|
|
|
|
|
def fetch_file(self, in_path, out_path):
|
|
|
|
return self._local.fetch_file(in_path, out_path)
|
|
|
|
|
2017-11-14 20:51:14 +00:00
|
|
|
def update_play_context(self, pc_data):
|
2016-12-06 02:42:09 +00:00
|
|
|
"""Updates the play context information for the connection"""
|
2017-11-14 20:51:14 +00:00
|
|
|
pc_data = to_bytes(pc_data)
|
|
|
|
if PY3:
|
|
|
|
pc_data = cPickle.loads(pc_data, encoding='bytes')
|
|
|
|
else:
|
|
|
|
pc_data = cPickle.loads(pc_data)
|
|
|
|
play_context = PlayContext()
|
|
|
|
play_context.deserialize(pc_data)
|
2017-03-21 02:26:18 +00:00
|
|
|
|
2017-11-14 20:51:14 +00:00
|
|
|
messages = ['updating play_context for connection']
|
2016-11-28 17:49:40 +00:00
|
|
|
if self._play_context.become is False and play_context.become is True:
|
|
|
|
auth_pass = play_context.become_pass
|
|
|
|
self._terminal.on_authorize(passwd=auth_pass)
|
2017-11-14 20:51:14 +00:00
|
|
|
messages.append('authorizing connection')
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
elif self._play_context.become is True and not play_context.become:
|
|
|
|
self._terminal.on_deauthorize()
|
2017-11-14 20:51:14 +00:00
|
|
|
messages.append('deauthorizing connection')
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
self._play_context = play_context
|
2017-11-14 20:51:14 +00:00
|
|
|
return messages
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
def _connect(self):
|
2017-11-09 20:04:40 +00:00
|
|
|
'''
|
|
|
|
Connects to the remote device and starts the terminal
|
|
|
|
'''
|
|
|
|
if self.connected:
|
|
|
|
return
|
2017-03-29 19:12:18 +00:00
|
|
|
|
2017-11-16 18:49:57 +00:00
|
|
|
p = connection_loader.get('paramiko', self._play_context, '/dev/null')
|
2017-11-27 13:17:17 +00:00
|
|
|
p.set_options(direct={'look_for_keys': not bool(self._play_context.password and not self._play_context.private_key_file)})
|
2017-11-17 18:09:02 +00:00
|
|
|
p.force_persistence = self.force_persistence
|
2017-11-16 18:49:57 +00:00
|
|
|
ssh = p._connect()
|
2016-11-30 21:28:47 +00:00
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
display.vvvv('ssh connection done, setting terminal', host=self._play_context.remote_addr)
|
2017-01-26 19:00:08 +00:00
|
|
|
|
2017-11-17 18:09:02 +00:00
|
|
|
self._ssh_shell = ssh.ssh.invoke_shell()
|
2017-11-09 20:04:40 +00:00
|
|
|
self._ssh_shell.settimeout(self._play_context.timeout)
|
2017-06-06 08:26:25 +00:00
|
|
|
|
2016-11-30 21:28:47 +00:00
|
|
|
network_os = self._play_context.network_os
|
|
|
|
if not network_os:
|
|
|
|
raise AnsibleConnectionFailure(
|
2017-01-13 00:31:35 +00:00
|
|
|
'Unable to automatically determine host network os. Please '
|
|
|
|
'manually configure ansible_network_os value for this host'
|
2016-11-30 21:28:47 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
self._terminal = terminal_loader.get(network_os, self)
|
|
|
|
if not self._terminal:
|
|
|
|
raise AnsibleConnectionFailure('network os %s is not supported' % network_os)
|
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
display.vvvv('loaded terminal plugin for network_os %s' % network_os, host=self._play_context.remote_addr)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-06-06 08:26:25 +00:00
|
|
|
self._cliconf = cliconf_loader.get(network_os, self)
|
|
|
|
if self._cliconf:
|
2017-11-09 20:04:40 +00:00
|
|
|
display.vvvv('loaded cliconf plugin for network_os %s' % network_os, host=self._play_context.remote_addr)
|
2017-06-06 08:26:25 +00:00
|
|
|
else:
|
2017-11-09 20:04:40 +00:00
|
|
|
display.vvvv('unable to load cliconf for network_os %s' % network_os)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
self.receive()
|
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
display.vvvv('firing event: on_open_shell()', host=self._play_context.remote_addr)
|
2017-06-06 08:26:25 +00:00
|
|
|
self._terminal.on_open_shell()
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
if self._play_context.become and self._play_context.become_method == 'enable':
|
|
|
|
display.vvvv('firing event: on_authorize', host=self._play_context.remote_addr)
|
2016-12-06 02:42:09 +00:00
|
|
|
auth_pass = self._play_context.become_pass
|
|
|
|
self._terminal.on_authorize(passwd=auth_pass)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
display.vvvv('ssh connection has completed successfully', host=self._play_context.remote_addr)
|
2017-06-06 08:26:25 +00:00
|
|
|
self._connected = True
|
2017-02-17 01:26:48 +00:00
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
def _update_connection_state(self):
|
|
|
|
'''
|
|
|
|
Reconstruct the connection socket_path and check if it exists
|
|
|
|
|
|
|
|
If the socket path exists then the connection is active and set
|
|
|
|
both the _socket_path value to the path and the _connected value
|
|
|
|
to True. If the socket path doesn't exist, leave the socket path
|
|
|
|
value to None and the _connected value to False
|
|
|
|
'''
|
|
|
|
ssh = connection_loader.get('ssh', class_only=True)
|
|
|
|
cp = ssh._create_control_path(self._play_context.remote_addr, self._play_context.port, self._play_context.remote_user)
|
|
|
|
|
|
|
|
tmp_path = unfrackpath(C.PERSISTENT_CONTROL_PATH_DIR)
|
|
|
|
socket_path = unfrackpath(cp % dict(directory=tmp_path))
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
if os.path.exists(socket_path):
|
|
|
|
self._connected = True
|
|
|
|
self._socket_path = socket_path
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-11-09 20:04:40 +00:00
|
|
|
def reset(self):
|
|
|
|
'''
|
|
|
|
Reset the connection
|
|
|
|
'''
|
|
|
|
if self._socket_path:
|
|
|
|
display.vvvv('resetting persistent connection for socket_path %s' % self._socket_path, host=self._play_context.remote_addr)
|
2017-11-22 15:30:06 +00:00
|
|
|
self.close()
|
|
|
|
display.vvvv('reset call on connection instance', host=self._play_context.remote_addr)
|
2017-11-09 20:04:40 +00:00
|
|
|
|
|
|
|
def close(self):
|
|
|
|
'''
|
|
|
|
Close the active connection to the device
|
|
|
|
'''
|
|
|
|
# only close the connection if its connected.
|
|
|
|
if self._connected:
|
|
|
|
display.debug("closing ssh connection to device")
|
|
|
|
if self._ssh_shell:
|
|
|
|
display.debug("firing event: on_close_shell()")
|
|
|
|
self._terminal.on_close_shell()
|
|
|
|
self._ssh_shell.close()
|
|
|
|
self._ssh_shell = None
|
|
|
|
display.debug("cli session is now closed")
|
|
|
|
self._connected = False
|
|
|
|
display.debug("ssh connection has been closed successfully")
|
2017-06-06 08:26:25 +00:00
|
|
|
|
|
|
|
def receive(self, command=None, prompts=None, answer=None):
|
2017-11-09 20:04:40 +00:00
|
|
|
'''
|
|
|
|
Handles receiving of output from command
|
|
|
|
'''
|
2017-05-12 16:13:51 +00:00
|
|
|
recv = BytesIO()
|
2016-11-28 17:49:40 +00:00
|
|
|
handled = False
|
|
|
|
|
|
|
|
self._matched_prompt = None
|
|
|
|
|
|
|
|
while True:
|
2017-11-09 20:04:40 +00:00
|
|
|
data = self._ssh_shell.recv(256)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-11-27 13:17:17 +00:00
|
|
|
# when a channel stream is closed, received data will be empty
|
|
|
|
if not data:
|
|
|
|
break
|
|
|
|
|
2016-11-28 17:49:40 +00:00
|
|
|
recv.write(data)
|
2016-12-04 01:05:33 +00:00
|
|
|
offset = recv.tell() - 256 if recv.tell() > 256 else 0
|
|
|
|
recv.seek(offset)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
window = self._strip(recv.read())
|
|
|
|
|
2017-06-06 08:26:25 +00:00
|
|
|
if prompts and not handled:
|
|
|
|
handled = self._handle_prompt(window, prompts, answer)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
if self._find_prompt(window):
|
|
|
|
self._last_response = recv.getvalue()
|
|
|
|
resp = self._strip(self._last_response)
|
2017-06-06 08:26:25 +00:00
|
|
|
return self._sanitize(resp, command)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-11-15 03:36:28 +00:00
|
|
|
def send(self, command, prompt=None, answer=None, send_only=False):
|
2017-11-09 20:04:40 +00:00
|
|
|
'''
|
|
|
|
Sends the command to the device in the opened shell
|
|
|
|
'''
|
2016-11-28 17:49:40 +00:00
|
|
|
try:
|
|
|
|
self._history.append(command)
|
2017-11-09 20:04:40 +00:00
|
|
|
self._ssh_shell.sendall(b'%s\r' % command)
|
2017-06-06 08:26:25 +00:00
|
|
|
if send_only:
|
2017-01-04 03:33:02 +00:00
|
|
|
return
|
2017-11-15 03:36:28 +00:00
|
|
|
response = self.receive(command, prompt, answer)
|
2017-11-09 20:04:40 +00:00
|
|
|
return to_text(response, errors='surrogate_or_strict')
|
2017-05-12 16:13:51 +00:00
|
|
|
except (socket.timeout, AttributeError):
|
2017-11-09 20:04:40 +00:00
|
|
|
display.vvvv(traceback.format_exc(), host=self._play_context.remote_addr)
|
2016-11-28 17:49:40 +00:00
|
|
|
raise AnsibleConnectionFailure("timeout trying to send command: %s" % command.strip())
|
|
|
|
|
|
|
|
def _strip(self, data):
|
2017-11-09 20:04:40 +00:00
|
|
|
'''
|
|
|
|
Removes ANSI codes from device response
|
|
|
|
'''
|
2016-11-28 17:49:40 +00:00
|
|
|
for regex in self._terminal.ansi_re:
|
2017-05-12 16:13:51 +00:00
|
|
|
data = regex.sub(b'', data)
|
2016-11-28 17:49:40 +00:00
|
|
|
return data
|
|
|
|
|
2017-05-24 14:10:38 +00:00
|
|
|
def _handle_prompt(self, resp, prompts, answer):
|
2017-11-09 20:04:40 +00:00
|
|
|
'''
|
2017-05-24 14:10:38 +00:00
|
|
|
Matches the command prompt and responds
|
|
|
|
|
|
|
|
:arg resp: Byte string containing the raw response from the remote
|
|
|
|
:arg prompts: Sequence of byte strings that we consider prompts for input
|
|
|
|
:arg answer: Byte string to send back to the remote if we find a prompt.
|
|
|
|
A carriage return is automatically appended to this string.
|
|
|
|
:returns: True if a prompt was found in ``resp``. False otherwise
|
2017-11-09 20:04:40 +00:00
|
|
|
'''
|
2017-11-13 19:24:13 +00:00
|
|
|
if not isinstance(prompts, list):
|
|
|
|
prompts = [prompts]
|
2017-05-24 14:10:38 +00:00
|
|
|
prompts = [re.compile(r, re.I) for r in prompts]
|
2017-02-21 13:27:33 +00:00
|
|
|
for regex in prompts:
|
|
|
|
match = regex.search(resp)
|
|
|
|
if match:
|
2017-11-09 20:04:40 +00:00
|
|
|
self._ssh_shell.sendall(b'%s\r' % answer)
|
2017-02-21 13:27:33 +00:00
|
|
|
return True
|
2017-05-24 14:10:38 +00:00
|
|
|
return False
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-06-06 08:26:25 +00:00
|
|
|
def _sanitize(self, resp, command=None):
|
2017-11-09 20:04:40 +00:00
|
|
|
'''
|
|
|
|
Removes elements from the response before returning to the caller
|
|
|
|
'''
|
2016-11-28 17:49:40 +00:00
|
|
|
cleaned = []
|
|
|
|
for line in resp.splitlines():
|
2017-08-09 13:43:04 +00:00
|
|
|
if (command and line.strip() == command.strip()) or self._matched_prompt.strip() in line:
|
2016-11-28 17:49:40 +00:00
|
|
|
continue
|
|
|
|
cleaned.append(line)
|
2017-05-24 14:10:38 +00:00
|
|
|
return b'\n'.join(cleaned).strip()
|
2016-11-28 17:49:40 +00:00
|
|
|
|
|
|
|
def _find_prompt(self, response):
|
2017-11-09 20:04:40 +00:00
|
|
|
'''Searches the buffered response for a matching command prompt
|
|
|
|
'''
|
2017-02-17 14:13:26 +00:00
|
|
|
errored_response = None
|
2017-05-24 14:10:38 +00:00
|
|
|
is_error_message = False
|
2017-02-17 15:00:23 +00:00
|
|
|
for regex in self._terminal.terminal_stderr_re:
|
2016-11-28 17:49:40 +00:00
|
|
|
if regex.search(response):
|
2017-05-24 14:10:38 +00:00
|
|
|
is_error_message = True
|
|
|
|
|
|
|
|
# Check if error response ends with command prompt if not
|
|
|
|
# receive it buffered prompt
|
|
|
|
for regex in self._terminal.terminal_stdout_re:
|
|
|
|
match = regex.search(response)
|
|
|
|
if match:
|
|
|
|
errored_response = response
|
2017-11-01 14:08:19 +00:00
|
|
|
self._matched_prompt = match.group()
|
2017-05-24 14:10:38 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
if not is_error_message:
|
|
|
|
for regex in self._terminal.terminal_stdout_re:
|
|
|
|
match = regex.search(response)
|
|
|
|
if match:
|
|
|
|
self._matched_pattern = regex.pattern
|
|
|
|
self._matched_prompt = match.group()
|
|
|
|
if not errored_response:
|
|
|
|
return True
|
2017-02-17 14:13:26 +00:00
|
|
|
|
|
|
|
if errored_response:
|
|
|
|
raise AnsibleConnectionFailure(errored_response)
|
2016-11-28 17:49:40 +00:00
|
|
|
|
2017-05-24 14:10:38 +00:00
|
|
|
return False
|