Support for executing job and adhoc commands on isolated Tower nodes (#6524)

This commit is contained in:
Ryan Petrello
2017-06-14 11:47:30 -04:00
committed by GitHub
parent aa962a26f1
commit 422950f45d
38 changed files with 1794 additions and 267 deletions

View File

@@ -0,0 +1,303 @@
import cStringIO
import mock
import os
import pytest
import re
import shutil
import stat
import tempfile
import time
from collections import OrderedDict
from Crypto.PublicKey import RSA
from Crypto import Random
from awx.main.isolated import run, isolated_manager
HERE, FILENAME = os.path.split(__file__)
@pytest.fixture(scope='function')
def rsa_key(request):
passphrase = 'passme'
key = RSA.generate(1024, Random.new().read)
return (key.exportKey('PEM', passphrase, pkcs=1), passphrase)
@pytest.fixture(scope='function')
def private_data_dir(request):
path = tempfile.mkdtemp(prefix='ansible_tower_unit_test')
request.addfinalizer(lambda: shutil.rmtree(path))
return path
@pytest.fixture(autouse=True)
def mock_sleep(request):
# the process teardown mechanism uses `time.sleep` to wait on processes to
# respond to SIGTERM; these are tests and don't care about being nice
m = mock.patch('time.sleep')
m.start()
request.addfinalizer(m.stop)
def test_simple_spawn():
stdout = cStringIO.StringIO()
status, rc = run.run_pexpect(
['ls', '-la'],
HERE,
{},
stdout,
cancelled_callback=lambda: False,
)
assert status == 'successful'
assert rc == 0
assert FILENAME in stdout.getvalue()
def test_error_rc():
stdout = cStringIO.StringIO()
status, rc = run.run_pexpect(
['ls', '-nonsense'],
HERE,
{},
stdout,
cancelled_callback=lambda: False,
)
assert status == 'failed'
# I'd expect 2, but we shouldn't risk making this test platform-dependent
assert rc > 0
def test_env_vars():
stdout = cStringIO.StringIO()
status, rc = run.run_pexpect(
['python', '-c', 'import os; print os.getenv("X_MY_ENV")'],
HERE,
{'X_MY_ENV': 'abc123'},
stdout,
cancelled_callback=lambda: False,
)
assert status == 'successful'
assert rc == 0
assert 'abc123' in stdout.getvalue()
def test_password_prompt():
stdout = cStringIO.StringIO()
expect_passwords = OrderedDict()
expect_passwords[re.compile(r'Password:\s*?$', re.M)] = 'secret123'
status, rc = run.run_pexpect(
['python', '-c', 'print raw_input("Password: ")'],
HERE,
{},
stdout,
cancelled_callback=lambda: False,
expect_passwords=expect_passwords
)
assert status == 'successful'
assert rc == 0
assert 'secret123' in stdout.getvalue()
def test_job_timeout():
stdout = cStringIO.StringIO()
extra_update_fields={}
status, rc = run.run_pexpect(
['python', '-c', 'import time; time.sleep(5)'],
HERE,
{},
stdout,
cancelled_callback=lambda: False,
extra_update_fields=extra_update_fields,
job_timeout=.01,
pexpect_timeout=0,
)
assert status == 'failed'
assert extra_update_fields == {'job_explanation': 'Job terminated due to timeout'}
def test_manual_cancellation():
stdout = cStringIO.StringIO()
status, rc = run.run_pexpect(
['python', '-c', 'print raw_input("Password: ")'],
HERE,
{},
stdout,
cancelled_callback=lambda: True, # this callable will cause cancellation
# the lack of password inputs will cause stdin to hang
pexpect_timeout=0,
)
assert status == 'canceled'
def test_build_isolated_job_data(private_data_dir, rsa_key):
pem, passphrase = rsa_key
mgr = isolated_manager.IsolatedManager(
['ls', '-la'], HERE, {}, cStringIO.StringIO(), ''
)
mgr.private_data_dir = private_data_dir
mgr.build_isolated_job_data()
path = os.path.join(private_data_dir, 'project')
assert os.path.isdir(path)
# <private_data_dir>/project is a soft link to HERE, which is the directory
# _this_ test file lives in
assert os.path.exists(os.path.join(path, FILENAME))
path = os.path.join(private_data_dir, 'artifacts')
assert os.path.isdir(path)
assert stat.S_IMODE(os.stat(path).st_mode) == stat.S_IRUSR + stat.S_IWUSR # user rw
path = os.path.join(private_data_dir, 'args')
with open(path, 'r') as f:
assert stat.S_IMODE(os.stat(path).st_mode) == stat.S_IRUSR # user r/o
assert f.read() == '["ls", "-la"]'
path = os.path.join(private_data_dir, '.rsync-filter')
with open(path, 'r') as f:
data = f.read()
assert data == '\n'.join([
'- /project/.git',
'- /project/.svn',
'- /project/.hg',
'- /artifacts/job_events/*-partial.json.tmp'
])
def test_run_isolated_job(private_data_dir, rsa_key):
env = {'JOB_ID': '1'}
pem, passphrase = rsa_key
mgr = isolated_manager.IsolatedManager(
['ls', '-la'], HERE, env, cStringIO.StringIO(), ''
)
mgr.private_data_dir = private_data_dir
secrets = {
'env': env,
'passwords': {
r'Enter passphrase for .*:\s*?$': passphrase
},
'ssh_key_data': pem
}
mgr.build_isolated_job_data()
stdout = cStringIO.StringIO()
# Mock environment variables for callback module
with mock.patch('os.getenv') as env_mock:
env_mock.return_value = '/path/to/awx/lib'
status, rc = run.run_isolated_job(private_data_dir, secrets, stdout)
assert status == 'successful'
assert rc == 0
assert FILENAME in stdout.getvalue()
assert '/path/to/awx/lib' in env['PYTHONPATH']
assert env['ANSIBLE_STDOUT_CALLBACK'] == 'tower_display'
assert env['ANSIBLE_CALLBACK_PLUGINS'] == '/path/to/awx/lib/isolated_callbacks'
assert env['AWX_ISOLATED_DATA_DIR'] == private_data_dir
def test_run_isolated_adhoc_command(private_data_dir, rsa_key):
env = {'AD_HOC_COMMAND_ID': '1'}
pem, passphrase = rsa_key
mgr = isolated_manager.IsolatedManager(
['pwd'], HERE, env, cStringIO.StringIO(), ''
)
mgr.private_data_dir = private_data_dir
secrets = {
'env': env,
'passwords': {
r'Enter passphrase for .*:\s*?$': passphrase
},
'ssh_key_data': pem
}
mgr.build_isolated_job_data()
stdout = cStringIO.StringIO()
# Mock environment variables for callback module
with mock.patch('os.getenv') as env_mock:
env_mock.return_value = '/path/to/awx/lib'
status, rc = run.run_isolated_job(private_data_dir, secrets, stdout)
assert status == 'successful'
assert rc == 0
# for ad-hoc jobs, `ansible` is invoked from the `private_data_dir`, so
# an ad-hoc command that runs `pwd` should print `private_data_dir` to stdout
assert private_data_dir in stdout.getvalue()
assert '/path/to/awx/lib' in env['PYTHONPATH']
assert env['ANSIBLE_STDOUT_CALLBACK'] == 'minimal'
assert env['ANSIBLE_CALLBACK_PLUGINS'] == '/path/to/awx/lib/isolated_callbacks'
assert env['AWX_ISOLATED_DATA_DIR'] == private_data_dir
def test_check_isolated_job(private_data_dir, rsa_key):
pem, passphrase = rsa_key
stdout = cStringIO.StringIO()
mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '')
mgr.private_data_dir = private_data_dir
mgr.instance = mock.Mock(pk=123, verbosity=5, spec_set=['pk', 'verbosity'])
mgr.started_at = time.time()
mgr.host = 'isolated-host'
os.mkdir(os.path.join(private_data_dir, 'artifacts'))
with mock.patch('awx.main.isolated.run.run_pexpect') as run_pexpect:
def _synchronize_job_artifacts(args, cwd, env, buff, **kw):
buff.write('checking job status...')
for filename, data in (
['status', 'failed'],
['rc', '1'],
['stdout', 'KABOOM!'],
):
with open(os.path.join(private_data_dir, 'artifacts', filename), 'w') as f:
f.write(data)
return ('successful', 0)
run_pexpect.side_effect = _synchronize_job_artifacts
with mock.patch.object(mgr, '_missing_artifacts') as missing_artifacts:
missing_artifacts.return_value = False
status, rc = mgr.check()
assert status == 'failed'
assert rc == 1
assert stdout.getvalue() == 'KABOOM!'
run_pexpect.assert_called_with(
[
'ansible-playbook', '-u', 'root', '-i', 'isolated-host,',
'check_isolated.yml', '-e', '{"src": "%s", "job_id": "123"}' % private_data_dir,
'-vvvvv'
],
'/tower_devel/awx/playbooks', mgr.env, mock.ANY,
cancelled_callback=None,
idle_timeout=0,
job_timeout=0,
pexpect_timeout=5,
proot_cmd='bwrap'
)
def test_check_isolated_job_timeout(private_data_dir, rsa_key):
pem, passphrase = rsa_key
stdout = cStringIO.StringIO()
extra_update_fields = {}
mgr = isolated_manager.IsolatedManager(['ls', '-la'], HERE, {}, stdout, '',
job_timeout=1,
extra_update_fields=extra_update_fields)
mgr.private_data_dir = private_data_dir
mgr.instance = mock.Mock(pk=123, verbosity=5, spec_set=['pk', 'verbosity'])
mgr.started_at = time.time()
mgr.host = 'isolated-host'
with mock.patch('awx.main.isolated.run.run_pexpect') as run_pexpect:
def _synchronize_job_artifacts(args, cwd, env, buff, **kw):
buff.write('checking job status...')
return ('failed', 1)
run_pexpect.side_effect = _synchronize_job_artifacts
status, rc = mgr.check()
assert status == 'failed'
assert rc == 1
assert stdout.getvalue() == 'checking job status...'
assert extra_update_fields['job_explanation'] == 'Job terminated due to timeout'

View File

@@ -3,14 +3,16 @@ from datetime import datetime
from functools import partial
import ConfigParser
import json
import os
import shutil
import tempfile
import os
import fcntl
import pytest
import mock
import pytest
import yaml
from awx.main.models import (
Credential,
CredentialType,
@@ -198,11 +200,21 @@ class TestJobExecution:
EXAMPLE_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxyz==\n-----END PRIVATE KEY-----'
def setup_method(self, method):
self.project_path = tempfile.mkdtemp(prefix='ansible_tower_project_')
with open(os.path.join(self.project_path, 'helloworld.yml'), 'w') as f:
f.write('---')
# The primary goal of these tests is to mock our `run_pexpect` call
# and make assertions about the arguments and environment passed to it.
self.run_pexpect = mock.Mock()
self.run_pexpect.return_value = ['successful', 0]
self.patches = [
mock.patch.object(Project, 'get_project_path', lambda *a, **kw: '/tmp/'),
mock.patch.object(Project, 'get_project_path', lambda *a, **kw: self.project_path),
# don't emit websocket statuses; they use the DB and complicate testing
mock.patch.object(UnifiedJob, 'websocket_emit_status', mock.Mock()),
mock.patch.object(Job, 'inventory', mock.Mock(pk=1, spec_set=['pk']))
mock.patch.object(Job, 'inventory', mock.Mock(pk=1, spec_set=['pk'])),
mock.patch('awx.main.isolated.run.run_pexpect', self.run_pexpect)
]
for p in self.patches:
p.start()
@@ -220,16 +232,13 @@ class TestJobExecution:
self.task = self.TASK_CLS()
self.task.update_model = mock.Mock(side_effect=status_side_effect)
# The primary goal of these tests is to mock our `run_pexpect` call
# and make assertions about the arguments and environment passed to it.
self.task.run_pexpect = mock.Mock(return_value=['successful', 0])
# ignore pre-run and post-run hooks, they complicate testing in a variety of ways
self.task.pre_run_hook = self.task.post_run_hook = self.task.final_run_hook = mock.Mock()
def teardown_method(self, method):
for p in self.patches:
p.stop()
shutil.rmtree(self.project_path, True)
def get_instance(self):
job = Job(
@@ -238,7 +247,9 @@ class TestJobExecution:
status='new',
job_type='run',
cancel_flag=False,
project=Project()
project=Project(),
playbook='helloworld.yml',
verbosity=3
)
# mock the job.extra_credentials M2M relation so we can avoid DB access
@@ -273,12 +284,107 @@ class TestGenericRun(TestJobExecution):
def test_uses_bubblewrap(self):
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert args[0] == 'bwrap'
class TestIsolatedExecution(TestJobExecution):
REMOTE_HOST = 'some-isolated-host'
def test_with_ssh_credentials(self):
mock_get = mock.Mock()
ssh = CredentialType.defaults['ssh']()
credential = Credential(
pk=1,
credential_type=ssh,
inputs = {
'username': 'bob',
'password': 'secret',
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
}
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.credential = credential
private_data = tempfile.mkdtemp(prefix='ansible_tower_')
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
inventory = json.dumps({"all": {"hosts": ["localhost"]}})
def _mock_job_artifacts(*args, **kw):
artifacts = os.path.join(private_data, 'artifacts')
if not os.path.exists(artifacts):
os.makedirs(artifacts)
if 'run_isolated.yml' in args[0]:
for filename, data in (
['status', 'successful'],
['rc', '0'],
['stdout', 'IT WORKED!'],
):
with open(os.path.join(artifacts, filename), 'w') as f:
f.write(data)
return ('successful', 0)
self.run_pexpect.side_effect = _mock_job_artifacts
with mock.patch('time.sleep'):
with mock.patch('requests.get') as mock_get:
mock_get.return_value = mock.Mock(content=inventory)
self.task.run(self.pk, self.REMOTE_HOST)
assert mock_get.call_count == 1
assert mock.call(
'http://127.0.0.1:8013/api/v1/inventories/1/script/?hostvars=1',
auth=mock.ANY
) in mock_get.call_args_list
playbook_run = self.run_pexpect.call_args_list[0][0]
assert ' '.join(playbook_run[0]).startswith(' '.join([
'ansible-playbook', '-u', 'root', '-i', self.REMOTE_HOST + ',',
'run_isolated.yml', '-e',
]))
extra_vars = playbook_run[0][playbook_run[0].index('-e') + 1]
extra_vars = json.loads(extra_vars)
assert extra_vars['dest'] == '/tmp'
assert extra_vars['src'] == private_data
assert extra_vars['proot_temp_dir'].startswith('/tmp/ansible_tower_proot_')
assert extra_vars['job_id'] == '1'
def test_systemctl_failure(self):
# If systemctl fails, read the contents of `artifacts/systemctl_logs`
mock_get = mock.Mock()
ssh = CredentialType.defaults['ssh']()
credential = Credential(
pk=1,
credential_type=ssh,
inputs = {'username': 'bob',}
)
self.instance.credential = credential
private_data = tempfile.mkdtemp(prefix='ansible_tower_')
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
inventory = json.dumps({"all": {"hosts": ["localhost"]}})
def _mock_job_artifacts(*args, **kw):
artifacts = os.path.join(private_data, 'artifacts')
if not os.path.exists(artifacts):
os.makedirs(artifacts)
if 'run_isolated.yml' in args[0]:
for filename, data in (
['systemctl_logs', 'ERROR IN EXPECT.PY'],
):
with open(os.path.join(artifacts, filename), 'w') as f:
f.write(data)
return ('successful', 0)
self.run_pexpect.side_effect = _mock_job_artifacts
with mock.patch('time.sleep'):
with mock.patch('requests.get') as mock_get:
mock_get.return_value = mock.Mock(content=inventory)
with pytest.raises(Exception):
self.task.run(self.pk, self.REMOTE_HOST)
class TestJobCredentials(TestJobExecution):
parametrize = {
@@ -301,11 +407,11 @@ class TestJobCredentials(TestJobExecution):
self.instance.credential = credential
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, call_kwargs = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert passwords[password_name] == 'secret'
assert 'secret' in call_kwargs.get('expect_passwords').values()
assert '-u bob' in ' '.join(args)
if expected_flag:
assert expected_flag in ' '.join(args)
@@ -324,7 +430,7 @@ class TestJobCredentials(TestJobExecution):
self.instance.credential = credential
def run_pexpect_side_effect(private_data, *args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
ssh_key_data_fifo = '/'.join([private_data, 'credential_1'])
assert open(ssh_key_data_fifo, 'r').read() == self.EXAMPLE_PRIVATE_KEY
assert ' '.join(args).startswith(
@@ -338,9 +444,7 @@ class TestJobCredentials(TestJobExecution):
private_data = tempfile.mkdtemp(prefix='ansible_tower_')
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
self.task.run_pexpect = mock.Mock(
side_effect=partial(run_pexpect_side_effect, private_data)
)
self.run_pexpect.side_effect = partial(run_pexpect_side_effect, private_data)
self.task.run(self.pk, private_data_dir=private_data)
def test_aws_cloud_credential(self):
@@ -354,9 +458,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['AWS_ACCESS_KEY'] == 'bob'
assert env['AWS_SECRET_KEY'] == 'secret'
@@ -374,9 +478,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['AWS_ACCESS_KEY'] == 'bob'
assert env['AWS_SECRET_KEY'] == 'secret'
@@ -397,14 +501,14 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert env['GCE_EMAIL'] == 'bob'
assert env['GCE_PROJECT'] == 'some-project'
ssh_key_data = env['GCE_PEM_FILE_PATH']
assert open(ssh_key_data, 'rb').read() == self.EXAMPLE_PRIVATE_KEY
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_azure_credentials(self):
@@ -421,13 +525,13 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert env['AZURE_SUBSCRIPTION_ID'] == 'bob'
ssh_key_data = env['AZURE_CERT_PATH']
assert open(ssh_key_data, 'rb').read() == self.EXAMPLE_PRIVATE_KEY
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_azure_rm_with_tenant(self):
@@ -447,9 +551,9 @@ class TestJobCredentials(TestJobExecution):
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['AZURE_CLIENT_ID'] == 'some-client'
assert env['AZURE_SECRET'] == 'some-secret'
@@ -472,9 +576,9 @@ class TestJobCredentials(TestJobExecution):
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['AZURE_SUBSCRIPTION_ID'] == 'some-subscription'
assert env['AZURE_AD_USER'] == 'bob'
@@ -491,9 +595,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['VMWARE_USER'] == 'bob'
assert env['VMWARE_PASSWORD'] == 'secret'
@@ -515,7 +619,7 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
shade_config = open(env['OS_CLIENT_CONFIG_FILE'], 'rb').read()
assert shade_config == '\n'.join([
'clouds:',
@@ -529,7 +633,7 @@ class TestJobCredentials(TestJobExecution):
])
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_net_credentials(self):
@@ -550,7 +654,7 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert env['ANSIBLE_NET_USERNAME'] == 'bob'
assert env['ANSIBLE_NET_PASSWORD'] == 'secret'
assert env['ANSIBLE_NET_AUTHORIZE'] == '1'
@@ -558,7 +662,7 @@ class TestJobCredentials(TestJobExecution):
assert open(env['ANSIBLE_NET_SSH_KEYFILE'], 'rb').read() == self.EXAMPLE_PRIVATE_KEY
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_custom_environment_injectors_with_jinja_syntax_error(self):
@@ -614,9 +718,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['MY_CLOUD_API_TOKEN'] == 'ABC123'
@@ -646,9 +750,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['JOB_ID'] == str(self.instance.pk)
@@ -680,9 +784,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert env['MY_CLOUD_PRIVATE_VAR'] == 'SUPER-SECRET-123'
assert 'SUPER-SECRET-123' not in json.dumps(self.task.update_model.call_args_list)
@@ -713,9 +817,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert '-e {"api_token": "ABC123"}' in ' '.join(args)
@@ -750,9 +854,9 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, _ = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert '-e {"password": "SUPER-SECRET-123"}' in ' '.join(args)
assert 'SUPER-SECRET-123' not in json.dumps(self.task.update_model.call_args_list)
@@ -787,11 +891,11 @@ class TestJobCredentials(TestJobExecution):
self.task.run(self.pk)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert open(env['MY_CLOUD_INI_FILE'], 'rb').read() == '[mycloud]\nABC123'
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_multi_cloud(self):
@@ -821,7 +925,7 @@ class TestJobCredentials(TestJobExecution):
self.instance.extra_credentials.add(azure_credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert env['GCE_EMAIL'] == 'bob'
assert env['GCE_PROJECT'] == 'some-project'
@@ -834,7 +938,7 @@ class TestJobCredentials(TestJobExecution):
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
@@ -874,12 +978,12 @@ class TestProjectUpdateCredentials(TestJobExecution):
)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
call_args, _ = self.task.run_pexpect.call_args_list[0]
job, args, cwd, env, passwords, stdout = call_args
assert self.run_pexpect.call_count == 1
call_args, call_kwargs = self.run_pexpect.call_args_list[0]
args, cwd, env, stdout = call_args
assert passwords.get('scm_username') == 'bob'
assert passwords.get('scm_password') == 'secret'
assert 'bob' in call_kwargs.get('expect_passwords').values()
assert 'secret' in call_kwargs.get('expect_passwords').values()
def test_ssh_key_auth(self, scm_type):
ssh = CredentialType.defaults['ssh']()
@@ -897,7 +1001,7 @@ class TestProjectUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(private_data, *args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
ssh_key_data_fifo = '/'.join([private_data, 'credential_1'])
assert open(ssh_key_data_fifo, 'r').read() == self.EXAMPLE_PRIVATE_KEY
assert ' '.join(args).startswith(
@@ -907,14 +1011,12 @@ class TestProjectUpdateCredentials(TestJobExecution):
ssh_key_data_fifo
)
)
assert passwords.get('scm_username') == 'bob'
assert 'bob' in kwargs.get('expect_passwords').values()
return ['successful', 0]
private_data = tempfile.mkdtemp(prefix='ansible_tower_')
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
self.task.run_pexpect = mock.Mock(
side_effect=partial(run_pexpect_side_effect, private_data)
)
self.run_pexpect.side_effect = partial(run_pexpect_side_effect, private_data)
self.task.run(self.pk)
@@ -944,7 +1046,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert env['AWS_ACCESS_KEY_ID'] == 'bob'
assert env['AWS_SECRET_ACCESS_KEY'] == 'secret'
@@ -955,7 +1057,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
assert 'ec2' in config.sections()
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_vmware_source(self):
@@ -971,7 +1073,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
config = ConfigParser.ConfigParser()
config.read(env['VMWARE_INI_PATH'])
@@ -980,7 +1082,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
assert config.get('vmware', 'server') == 'https://example.org'
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_azure_source(self):
@@ -999,13 +1101,13 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert env['AZURE_SUBSCRIPTION_ID'] == 'bob'
ssh_key_data = env['AZURE_CERT_PATH']
assert open(ssh_key_data, 'rb').read() == self.EXAMPLE_PRIVATE_KEY
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_gce_source(self):
@@ -1025,14 +1127,14 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
assert env['GCE_EMAIL'] == 'bob'
assert env['GCE_PROJECT'] == 'some-project'
ssh_key_data = env['GCE_PEM_FILE_PATH']
assert open(ssh_key_data, 'rb').read() == self.EXAMPLE_PRIVATE_KEY
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_openstack_source(self):
@@ -1053,7 +1155,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
shade_config = open(env['OS_CLIENT_CONFIG_FILE'], 'rb').read()
assert '\n'.join([
'clouds:',
@@ -1067,7 +1169,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
]) in shade_config
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_satellite6_source(self):
@@ -1087,7 +1189,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
config = ConfigParser.ConfigParser()
config.read(env['FOREMAN_INI_PATH'])
assert config.get('foreman', 'url') == 'https://example.org'
@@ -1095,7 +1197,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
assert config.get('foreman', 'password') == 'secret'
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)
def test_cloudforms_source(self):
@@ -1115,7 +1217,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
args, cwd, env, stdout = args
config = ConfigParser.ConfigParser()
config.read(env['CLOUDFORMS_INI_PATH'])
assert config.get('cloudforms', 'url') == 'https://example.org'
@@ -1124,7 +1226,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
assert config.get('cloudforms', 'ssl_verify') == 'false'
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.run_pexpect.side_effect = run_pexpect_side_effect
self.task.run(self.pk)

View File

@@ -0,0 +1,112 @@
import cStringIO
import pytest
import base64
import json
from awx.main.utils import OutputEventFilter
MAX_WIDTH = 78
EXAMPLE_UUID = '890773f5-fe6d-4091-8faf-bdc8021d65dd'
def write_encoded_event_data(fileobj, data):
b64data = base64.b64encode(json.dumps(data))
# pattern corresponding to OutputEventFilter expectation
fileobj.write(u'\x1b[K')
for offset in xrange(0, len(b64data), MAX_WIDTH):
chunk = b64data[offset:offset + MAX_WIDTH]
escaped_chunk = u'{}\x1b[{}D'.format(chunk, len(chunk))
fileobj.write(escaped_chunk)
fileobj.write(u'\x1b[K')
@pytest.fixture
def fake_callback():
return []
@pytest.fixture
def fake_cache():
return {}
@pytest.fixture
def wrapped_handle(job_event_callback):
# Preliminary creation of resources usually done in tasks.py
stdout_handle = cStringIO.StringIO()
return OutputEventFilter(stdout_handle, job_event_callback)
@pytest.fixture
def job_event_callback(fake_callback, fake_cache):
def method(event_data):
if 'uuid' in event_data:
cache_event = fake_cache.get(':1:ev-{}'.format(event_data['uuid']), None)
if cache_event is not None:
event_data.update(cache_event)
fake_callback.append(event_data)
return method
def test_event_recomb(fake_callback, fake_cache, wrapped_handle):
# Pretend that this is done by the Ansible callback module
fake_cache[':1:ev-{}'.format(EXAMPLE_UUID)] = {'event': 'foo'}
write_encoded_event_data(wrapped_handle, {
'uuid': EXAMPLE_UUID
})
wrapped_handle.write('\r\nTASK [Gathering Facts] *********************************************************\n')
wrapped_handle.write('\u001b[0;33mchanged: [localhost]\u001b[0m\n')
write_encoded_event_data(wrapped_handle, {})
# stop pretending
assert len(fake_callback) == 1
recomb_data = fake_callback[0]
assert 'event' in recomb_data
assert recomb_data['event'] == 'foo'
def test_separate_verbose_events(fake_callback, wrapped_handle):
# Pretend that this is done by the Ansible callback module
wrapped_handle.write('Using /etc/ansible/ansible.cfg as config file\n')
wrapped_handle.write('SSH password: \n')
write_encoded_event_data(wrapped_handle, { # associated with _next_ event
'uuid': EXAMPLE_UUID
})
# stop pretending
assert len(fake_callback) == 2
for event_data in fake_callback:
assert 'event' in event_data
assert event_data['event'] == 'verbose'
def test_verbose_event_no_markings(fake_callback, wrapped_handle):
'''
This occurs with jobs that do not have events but still generate
and output stream, like system jobs
'''
wrapped_handle.write('Running tower-manage command \n')
assert wrapped_handle._fileobj.getvalue() == 'Running tower-manage command \n'
def test_large_data_payload(fake_callback, fake_cache, wrapped_handle):
# Pretend that this is done by the Ansible callback module
fake_cache[':1:ev-{}'.format(EXAMPLE_UUID)] = {'event': 'foo'}
event_data_to_encode = {
'uuid': EXAMPLE_UUID,
'host': 'localhost',
'role': 'some_path_to_role'
}
assert len(json.dumps(event_data_to_encode)) > MAX_WIDTH
write_encoded_event_data(wrapped_handle, event_data_to_encode)
wrapped_handle.write('\r\nTASK [Gathering Facts] *********************************************************\n')
wrapped_handle.write('\u001b[0;33mchanged: [localhost]\u001b[0m\n')
write_encoded_event_data(wrapped_handle, {})
# stop pretending
assert len(fake_callback) == 1
recomb_data = fake_callback[0]
assert 'role' in recomb_data
assert recomb_data['role'] == 'some_path_to_role'
assert 'event' in recomb_data
assert recomb_data['event'] == 'foo'