Merge pull request #2396 from ryanpetrello/fix-pexpect-hang

support env vars that contain unicode (without hanging)
This commit is contained in:
Ryan Petrello
2018-07-03 08:44:46 -04:00
committed by GitHub
2 changed files with 32 additions and 20 deletions

View File

@@ -4,7 +4,7 @@ import argparse
import base64 import base64
import codecs import codecs
import collections import collections
import cStringIO import StringIO
import logging import logging
import json import json
import os import os
@@ -18,6 +18,7 @@ import time
import pexpect import pexpect
import psutil import psutil
import six
logger = logging.getLogger('awx.main.utils.expect') logger = logging.getLogger('awx.main.utils.expect')
@@ -99,6 +100,12 @@ def run_pexpect(args, cwd, env, logfile,
password_patterns = expect_passwords.keys() password_patterns = expect_passwords.keys()
password_values = expect_passwords.values() password_values = expect_passwords.values()
# pexpect needs all env vars to be utf-8 encoded strings
# https://github.com/pexpect/pexpect/issues/512
for k, v in env.items():
if isinstance(v, six.text_type):
env[k] = v.encode('utf-8')
child = pexpect.spawn( child = pexpect.spawn(
args[0], args[1:], cwd=cwd, env=env, ignore_sighup=True, args[0], args[1:], cwd=cwd, env=env, ignore_sighup=True,
encoding='utf-8', echo=False, use_poll=True encoding='utf-8', echo=False, use_poll=True
@@ -240,7 +247,7 @@ def handle_termination(pid, args, proot_cmd, is_cancel=True):
def __run__(private_data_dir): def __run__(private_data_dir):
buff = cStringIO.StringIO() buff = StringIO.StringIO()
with open(os.path.join(private_data_dir, 'env'), 'r') as f: with open(os.path.join(private_data_dir, 'env'), 'r') as f:
for line in f: for line in f:
buff.write(line) buff.write(line)

View File

@@ -1,4 +1,6 @@
import cStringIO # -*- coding: utf-8 -*-
import StringIO
import mock import mock
import os import os
import pytest import pytest
@@ -16,6 +18,7 @@ from cryptography.hazmat.primitives import serialization
from awx.main.expect import run, isolated_manager from awx.main.expect import run, isolated_manager
from django.conf import settings from django.conf import settings
import six
HERE, FILENAME = os.path.split(__file__) HERE, FILENAME = os.path.split(__file__)
@@ -55,7 +58,7 @@ def mock_sleep(request):
def test_simple_spawn(): def test_simple_spawn():
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
status, rc = run.run_pexpect( status, rc = run.run_pexpect(
['ls', '-la'], ['ls', '-la'],
HERE, HERE,
@@ -69,7 +72,7 @@ def test_simple_spawn():
def test_error_rc(): def test_error_rc():
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
status, rc = run.run_pexpect( status, rc = run.run_pexpect(
['ls', '-nonsense'], ['ls', '-nonsense'],
HERE, HERE,
@@ -83,7 +86,7 @@ def test_error_rc():
def test_cancel_callback_error(): def test_cancel_callback_error():
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
def bad_callback(): def bad_callback():
raise Exception('unique exception') raise Exception('unique exception')
@@ -102,22 +105,24 @@ def test_cancel_callback_error():
assert extra_fields['job_explanation'] == "System error during job execution, check system logs" assert extra_fields['job_explanation'] == "System error during job execution, check system logs"
def test_env_vars(): @pytest.mark.timeout(3) # https://github.com/ansible/tower/issues/2391#issuecomment-401946895
stdout = cStringIO.StringIO() @pytest.mark.parametrize('value', ['abc123', six.u('Iñtërnâtiônàlizætiøn')])
def test_env_vars(value):
stdout = StringIO.StringIO()
status, rc = run.run_pexpect( status, rc = run.run_pexpect(
['python', '-c', 'import os; print os.getenv("X_MY_ENV")'], ['python', '-c', 'import os; print os.getenv("X_MY_ENV")'],
HERE, HERE,
{'X_MY_ENV': 'abc123'}, {'X_MY_ENV': value},
stdout, stdout,
cancelled_callback=lambda: False, cancelled_callback=lambda: False,
) )
assert status == 'successful' assert status == 'successful'
assert rc == 0 assert rc == 0
assert 'abc123' in stdout.getvalue() assert value in stdout.getvalue()
def test_password_prompt(): def test_password_prompt():
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
expect_passwords = OrderedDict() expect_passwords = OrderedDict()
expect_passwords[re.compile(r'Password:\s*?$', re.M)] = 'secret123' expect_passwords[re.compile(r'Password:\s*?$', re.M)] = 'secret123'
status, rc = run.run_pexpect( status, rc = run.run_pexpect(
@@ -134,7 +139,7 @@ def test_password_prompt():
def test_job_timeout(): def test_job_timeout():
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
extra_update_fields={} extra_update_fields={}
status, rc = run.run_pexpect( status, rc = run.run_pexpect(
['python', '-c', 'import time; time.sleep(5)'], ['python', '-c', 'import time; time.sleep(5)'],
@@ -151,7 +156,7 @@ def test_job_timeout():
def test_manual_cancellation(): def test_manual_cancellation():
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
status, rc = run.run_pexpect( status, rc = run.run_pexpect(
['python', '-c', 'print raw_input("Password: ")'], ['python', '-c', 'print raw_input("Password: ")'],
HERE, HERE,
@@ -167,7 +172,7 @@ def test_manual_cancellation():
def test_build_isolated_job_data(private_data_dir, rsa_key): def test_build_isolated_job_data(private_data_dir, rsa_key):
pem, passphrase = rsa_key pem, passphrase = rsa_key
mgr = isolated_manager.IsolatedManager( mgr = isolated_manager.IsolatedManager(
['ls', '-la'], HERE, {}, cStringIO.StringIO(), '' ['ls', '-la'], HERE, {}, StringIO.StringIO(), ''
) )
mgr.private_data_dir = private_data_dir mgr.private_data_dir = private_data_dir
mgr.build_isolated_job_data() mgr.build_isolated_job_data()
@@ -204,7 +209,7 @@ def test_run_isolated_job(private_data_dir, rsa_key):
env = {'JOB_ID': '1'} env = {'JOB_ID': '1'}
pem, passphrase = rsa_key pem, passphrase = rsa_key
mgr = isolated_manager.IsolatedManager( mgr = isolated_manager.IsolatedManager(
['ls', '-la'], HERE, env, cStringIO.StringIO(), '' ['ls', '-la'], HERE, env, StringIO.StringIO(), ''
) )
mgr.private_data_dir = private_data_dir mgr.private_data_dir = private_data_dir
secrets = { secrets = {
@@ -215,7 +220,7 @@ def test_run_isolated_job(private_data_dir, rsa_key):
'ssh_key_data': pem 'ssh_key_data': pem
} }
mgr.build_isolated_job_data() mgr.build_isolated_job_data()
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
# Mock environment variables for callback module # Mock environment variables for callback module
with mock.patch('os.getenv') as env_mock: with mock.patch('os.getenv') as env_mock:
env_mock.return_value = '/path/to/awx/lib' env_mock.return_value = '/path/to/awx/lib'
@@ -234,7 +239,7 @@ def test_run_isolated_adhoc_command(private_data_dir, rsa_key):
env = {'AD_HOC_COMMAND_ID': '1'} env = {'AD_HOC_COMMAND_ID': '1'}
pem, passphrase = rsa_key pem, passphrase = rsa_key
mgr = isolated_manager.IsolatedManager( mgr = isolated_manager.IsolatedManager(
['pwd'], HERE, env, cStringIO.StringIO(), '' ['pwd'], HERE, env, StringIO.StringIO(), ''
) )
mgr.private_data_dir = private_data_dir mgr.private_data_dir = private_data_dir
secrets = { secrets = {
@@ -245,7 +250,7 @@ def test_run_isolated_adhoc_command(private_data_dir, rsa_key):
'ssh_key_data': pem 'ssh_key_data': pem
} }
mgr.build_isolated_job_data() mgr.build_isolated_job_data()
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
# Mock environment variables for callback module # Mock environment variables for callback module
with mock.patch('os.getenv') as env_mock: with mock.patch('os.getenv') as env_mock:
env_mock.return_value = '/path/to/awx/lib' env_mock.return_value = '/path/to/awx/lib'
@@ -265,7 +270,7 @@ def test_run_isolated_adhoc_command(private_data_dir, rsa_key):
def test_check_isolated_job(private_data_dir, rsa_key): def test_check_isolated_job(private_data_dir, rsa_key):
pem, passphrase = rsa_key pem, passphrase = rsa_key
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '') mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '')
mgr.private_data_dir = private_data_dir mgr.private_data_dir = private_data_dir
mgr.instance = mock.Mock(id=123, pk=123, verbosity=5, spec_set=['id', 'pk', 'verbosity']) mgr.instance = mock.Mock(id=123, pk=123, verbosity=5, spec_set=['id', 'pk', 'verbosity'])
@@ -315,7 +320,7 @@ def test_check_isolated_job(private_data_dir, rsa_key):
def test_check_isolated_job_timeout(private_data_dir, rsa_key): def test_check_isolated_job_timeout(private_data_dir, rsa_key):
pem, passphrase = rsa_key pem, passphrase = rsa_key
stdout = cStringIO.StringIO() stdout = StringIO.StringIO()
extra_update_fields = {} extra_update_fields = {}
mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '', mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '',
job_timeout=1, job_timeout=1,