adds two new plugins that use ansible-connection for persistence (#18572)

* adds new connection plugin `network_cli` which builds on paramiko
* adds new plugin `terminal` used for manipulating network_cli terminals
* adds new field to play_context `network_os` settable as ansible_network_os

This commit adds the plugins necesary to establish a persistent cli connection
to network devices of ssh.  It builds on the paramiko connection plugin
to create a shell environment that will persistent through ansible-connection.
The `newtork_cli` plugin then uses the network_os in the instance of
PlayContext to load the appropriate network OS environment plugin for
handling opening and closing of shells as well as privilege escalation.
This commit is contained in:
Peter Sprygada 2016-11-28 12:49:40 -05:00 committed by GitHub
parent 54c5ea29bb
commit 9aa8547016
13 changed files with 764 additions and 2 deletions

View file

@ -0,0 +1,190 @@
#
# (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 re
import socket
import json
import signal
import datetime
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils.six.moves import StringIO
from ansible.plugins import terminal_loader
from ansible.plugins.connection.paramiko_ssh import Connection as _Connection
class Connection(_Connection):
''' CLI SSH based connections on Paramiko '''
transport = 'network_cli'
has_pipelining = False
def __init__(self, play_context, new_stdin, *args, **kwargs):
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
assert self._play_context.network_os, 'ansible_network_os must be set'
self._terminal = terminal_loader.get(self._play_context.network_os, self)
if not self._terminal:
raise AnsibleConnectionFailure('network os %s is not supported' % self._play_context.network_os)
self._shell = None
self._matched_prompt = None
self._matched_pattern = None
self._last_response = None
self._history = list()
def update_play_context(self, play_context):
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)
elif self._play_context.become is True and not play_context.become:
self._terminal.on_deauthorize()
self._play_context = play_context
def _connect(self):
super(Connection, self)._connect()
return (0, 'connected', '')
def open_shell(self, timeout=10):
self._shell = self.ssh.invoke_shell()
self._shell.settimeout(self._play_context.timeout)
self.receive()
if self._shell:
self._terminal.on_open_shell()
if hasattr(self._play_context, 'become'):
if self._play_context.become:
auth_pass = self._play_context.become_pass
self._terminal.on_authorize(passwd=auth_pass)
def close(self):
self.close_shell()
super(Connection, self).close()
def close_shell(self):
if self._shell:
self._terminal.on_close_shell()
if self._terminal.supports_multiplexing and self._shell:
self._shell.close()
self._shell = None
return (0, 'shell closed', '')
def receive(self, obj=None):
recv = StringIO()
handled = False
self._matched_prompt = None
while True:
data = self._shell.recv(256)
recv.write(data)
recv.seek(recv.tell() - 256)
window = self._strip(recv.read())
if obj and (obj.get('prompt') and not handled):
handled = self._handle_prompt(window, obj)
if self._find_prompt(window):
self._last_response = recv.getvalue()
resp = self._strip(self._last_response)
return self._sanitize(resp, obj)
def send(self, obj):
try:
command = obj['command']
self._history.append(command)
self._shell.sendall('%s\r' % command)
return self.receive(obj)
except (socket.timeout, AttributeError):
raise AnsibleConnectionFailure("timeout trying to send command: %s" % command.strip())
def _strip(self, data):
for regex in self._terminal.ansi_re:
data = regex.sub('', data)
return data
def _handle_prompt(self, resp, obj):
prompt = re.compile(obj['prompt'], re.I)
answer = obj['answer']
match = prompt.search(resp)
if match:
self._shell.sendall('%s\r' % answer)
return True
def _sanitize(self, resp, obj=None):
cleaned = []
command = obj.get('command') if obj else None
for line in resp.splitlines():
if (command and line.startswith(command.strip())) or self._find_prompt(line):
continue
cleaned.append(line)
return str("\n".join(cleaned)).strip()
def _find_prompt(self, response):
for regex in self._terminal.terminal_errors_re:
if regex.search(response):
raise AnsibleConnectionFailure(response)
for regex in self._terminal.terminal_prompts_re:
match = regex.search(response)
if match:
self._matched_pattern = regex.pattern
self._matched_prompt = match.group()
return True
def alarm_handler(self, signum, frame):
self.close_shell()
def exec_command(self, cmd):
''' {'command': <str>, 'prompt': <str>, 'answer': <str>} '''
try:
obj = json.loads(cmd)
except ValueError:
obj = {'command': str(cmd).strip()}
if obj['command'] == 'close_shell()':
return self.close_shell()
elif obj['command'] == 'prompt()':
return (0, self._matched_prompt, '')
elif obj['command'] == 'history()':
return (0, self._history, '')
try:
if self._shell is None:
self.open_shell()
except AnsibleConnectionFailure as exc:
return (1, '', str(exc))
try:
out = self.send(obj)
return (0, out, '')
except (AnsibleConnectionFailure, ValueError) as exc:
return (1, '', str(exc))

View file

@ -421,8 +421,9 @@ class Connection(ConnectionBase):
SSH_CONNECTION_CACHE.pop(cache_key, None)
SFTP_CONNECTION_CACHE.pop(cache_key, None)
if self.sftp is not None:
self.sftp.close()
if hasattr(self, 'sftp'):
if self.sftp is not None:
self.sftp.close()
if C.HOST_KEY_CHECKING and C.PARAMIKO_RECORD_HOST_KEYS and self._any_keys_added():