mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-23 19:01:26 -07:00
Fix for persistent connection plugin on Python3 (#24431)
Fix for persistent connection plugin on Python3. Note that fixes are also needed to each terminal plugin. This PR only fixes the ios terminal (as proof that this approach is workable.) Future PRs can address the other terminal types. * On Python3, pickle needs to work with byte strings, not text strings. * Set the pickle protocol version to 0 because we're using a pty to feed data to the connection plugin. A pty can't have control characters. So we have to send ascii only. That means only using protocol=0 for pickling the data. * ansible-connection isn't being used with py3 in the bug but it needs several changes to work with python3. * In python3, closing the pty too early causes no data to be sent. So leave stdin open until after we finish with the ansible-connection process. * Fix typo using traceback.format_exc() * Cleanup unnecessary StringIO, BytesIO, and to_bytes calls * Modify the network_cli and terminal plugins for py3 compat. Lots of mixing of text and byte strings that needs to be straightened out to be compatible with python3 * Documentation for the bytes<=>text strategy for terminal plugins * Update unittests for more bytes-oriented internals Fixes #24355
This commit is contained in:
parent
e539726543
commit
d834412ead
6 changed files with 146 additions and 112 deletions
|
@ -18,17 +18,18 @@
|
|||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
import socket
|
||||
import json
|
||||
import signal
|
||||
import datetime
|
||||
import traceback
|
||||
import logging
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import traceback
|
||||
from collections import Sequence
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.module_utils.six.moves import StringIO
|
||||
from ansible.module_utils.six import BytesIO, binary_type, text_type
|
||||
from ansible.module_utils._text import to_bytes, to_text
|
||||
from ansible.plugins import terminal_loader
|
||||
from ansible.plugins.connection import ensure_connect
|
||||
from ansible.plugins.connection.paramiko_ssh import Connection as _Connection
|
||||
|
@ -113,7 +114,7 @@ class Connection(_Connection):
|
|||
self._terminal.on_authorize(passwd=auth_pass)
|
||||
|
||||
display.display('shell successfully opened', log_only=True)
|
||||
return (0, 'ok', '')
|
||||
return (0, b'ok', b'')
|
||||
|
||||
def close(self):
|
||||
display.display('closing connection', log_only=True)
|
||||
|
@ -131,11 +132,11 @@ class Connection(_Connection):
|
|||
self._shell.close()
|
||||
self._shell = None
|
||||
|
||||
return (0, 'ok', '')
|
||||
return (0, b'ok', b'')
|
||||
|
||||
def receive(self, obj=None):
|
||||
"""Handles receiving of output from command"""
|
||||
recv = StringIO()
|
||||
recv = BytesIO()
|
||||
handled = False
|
||||
|
||||
self._matched_prompt = None
|
||||
|
@ -162,30 +163,30 @@ class Connection(_Connection):
|
|||
try:
|
||||
command = obj['command']
|
||||
self._history.append(command)
|
||||
self._shell.sendall('%s\r' % command)
|
||||
self._shell.sendall(b'%s\r' % command)
|
||||
if obj.get('sendonly'):
|
||||
return
|
||||
return self.receive(obj)
|
||||
except (socket.timeout, AttributeError) as exc:
|
||||
except (socket.timeout, AttributeError):
|
||||
display.display(traceback.format_exc(), log_only=True)
|
||||
raise AnsibleConnectionFailure("timeout trying to send command: %s" % command.strip())
|
||||
|
||||
def _strip(self, data):
|
||||
"""Removes ANSI codes from device response"""
|
||||
for regex in self._terminal.ansi_re:
|
||||
data = regex.sub('', data)
|
||||
data = regex.sub(b'', data)
|
||||
return data
|
||||
|
||||
def _handle_prompt(self, resp, obj):
|
||||
"""Matches the command prompt and responds"""
|
||||
if not isinstance(obj['prompt'], list):
|
||||
if isinstance(obj, (binary_type, text_type)) or not isinstance(obj['prompt'], Sequence):
|
||||
obj['prompt'] = [obj['prompt']]
|
||||
prompts = [re.compile(r, re.I) for r in obj['prompt']]
|
||||
answer = obj['answer']
|
||||
for regex in prompts:
|
||||
match = regex.search(resp)
|
||||
if match:
|
||||
self._shell.sendall('%s\r' % answer)
|
||||
self._shell.sendall(b'%s\r' % answer)
|
||||
return True
|
||||
|
||||
def _sanitize(self, resp, obj=None):
|
||||
|
@ -196,7 +197,7 @@ class Connection(_Connection):
|
|||
if (command and line.startswith(command.strip())) or self._matched_prompt.strip() in line:
|
||||
continue
|
||||
cleaned.append(line)
|
||||
return str("\n".join(cleaned)).strip()
|
||||
return b"\n".join(cleaned).strip()
|
||||
|
||||
def _find_prompt(self, response):
|
||||
"""Searches the buffered response for a matching command prompt"""
|
||||
|
@ -225,9 +226,9 @@ class Connection(_Connection):
|
|||
def exec_command(self, cmd):
|
||||
"""Executes the cmd on in the shell and returns the output
|
||||
|
||||
The method accepts two forms of cmd. The first form is as a
|
||||
The method accepts two forms of cmd. The first form is as a byte
|
||||
string that represents the command to be executed in the shell. The
|
||||
second form is as a JSON string with additional keyword.
|
||||
second form is as a utf8 JSON byte string with additional keywords.
|
||||
|
||||
Keywords supported for cmd:
|
||||
* command - the command string to execute
|
||||
|
@ -235,28 +236,30 @@ class Connection(_Connection):
|
|||
* answer - the string to respond to the prompt with
|
||||
* sendonly - bool to disable waiting for response
|
||||
|
||||
:arg cmd: the string that represents the command to be executed
|
||||
which can be a single command or a json encoded string
|
||||
:arg cmd: the byte string that represents the command to be executed
|
||||
which can be a single command or a json encoded string.
|
||||
:returns: a tuple of (return code, stdout, stderr). The return
|
||||
code is an integer and stdout and stderr are strings
|
||||
code is an integer and stdout and stderr are byte strings
|
||||
"""
|
||||
try:
|
||||
obj = json.loads(cmd)
|
||||
obj = json.loads(to_text(cmd, errors='surrogate_or_strict'))
|
||||
obj = dict((k, to_bytes(v, errors='surrogate_or_strict', nonstring='passthru')) for k, v in obj.items())
|
||||
except (ValueError, TypeError):
|
||||
obj = {'command': str(cmd).strip()}
|
||||
obj = {'command': to_bytes(cmd.strip(), errors='surrogate_or_strict')}
|
||||
|
||||
if obj['command'] == 'close_shell()':
|
||||
if obj['command'] == b'close_shell()':
|
||||
return self.close_shell()
|
||||
elif obj['command'] == 'open_shell()':
|
||||
elif obj['command'] == b'open_shell()':
|
||||
return self.open_shell()
|
||||
elif obj['command'] == 'prompt()':
|
||||
return (0, self._matched_prompt, '')
|
||||
elif obj['command'] == b'prompt()':
|
||||
return (0, self._matched_prompt, b'')
|
||||
|
||||
try:
|
||||
if self._shell is None:
|
||||
self.open_shell()
|
||||
except AnsibleConnectionFailure as exc:
|
||||
return (1, '', str(exc))
|
||||
# FIXME: Feels like we should raise this rather than return it
|
||||
return (1, b'', to_bytes(exc))
|
||||
|
||||
try:
|
||||
if not signal.getsignal(signal.SIGALRM):
|
||||
|
@ -264,6 +267,7 @@ class Connection(_Connection):
|
|||
signal.alarm(self._play_context.timeout)
|
||||
out = self.send(obj)
|
||||
signal.alarm(0)
|
||||
return (0, out, '')
|
||||
return (0, out, b'')
|
||||
except (AnsibleConnectionFailure, ValueError) as exc:
|
||||
return (1, '', str(exc))
|
||||
# FIXME: Feels like we should raise this rather than return it
|
||||
return (1, b'', to_bytes(exc))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue