New function: run_ansible_playbook

This new function allows to run ansible playbooks
in a convenient way, taking advantage of already existing
run_command_and_log - that latter method has been modified so
that we can get the process full state instead of just the exit
code.

It implements some checks in order to ensure the playbook exists,
and allows to set a temporary ansible configuration file in order
to avoid unwanted behaviors (like retry file creation, callback for
output, and so on).

Change-Id: I5de4d68cc2669d37e7a667209423b9ac842136ff
This commit is contained in:
Cédric Jeanneret 2018-07-27 14:48:01 +02:00
parent 1e27b1e571
commit edc7365c90
2 changed files with 221 additions and 9 deletions

View File

@ -17,6 +17,7 @@
import argparse
import datetime
import mock
import os
import os.path
import tempfile
@ -32,6 +33,134 @@ from tripleoclient import exceptions
from tripleoclient import utils
class TestRunAnsiblePlaybook(TestCase):
def setUp(self):
self.mock_log = mock.Mock('logging.getLogger')
@mock.patch('os.path.exists', return_value=False)
@mock.patch('tripleoclient.utils.run_command_and_log')
def test_no_playbook(self, mock_run, mock_exists):
self.assertRaises(RuntimeError,
utils.run_ansible_playbook,
self.mock_log,
'/tmp',
'non-existing.yaml',
'localhost,'
)
mock_exists.assert_called_once_with('/tmp/non-existing.yaml')
mock_run.assert_not_called()
@mock.patch('tempfile.mkstemp', return_value=('foo', '/tmp/fooBar.cfg'))
@mock.patch('os.path.exists', return_value=True)
@mock.patch('tripleoclient.utils.run_command_and_log')
def test_subprocess_error(self, mock_run, mock_exists, mock_mkstemp):
mock_process = mock.Mock()
mock_process.returncode = 1
mock_process.stdout.read.side_effect = ["Error\n"]
mock_run.return_value = mock_process
env = os.environ.copy()
env['ANSIBLE_CONFIG'] = '/tmp/fooBar.cfg'
self.assertRaises(RuntimeError,
utils.run_ansible_playbook,
self.mock_log,
'/tmp',
'existing.yaml',
'localhost,'
)
mock_run.assert_called_once_with(self.mock_log,
['ansible-playbook', '-i',
'localhost,', '-c', 'smart',
'/tmp/existing.yaml'],
env=env, retcode_only=False)
@mock.patch('os.path.isabs')
@mock.patch('os.path.exists', return_value=False)
@mock.patch('tripleoclient.utils.run_command_and_log')
def test_non_existing_config(self, mock_run, mock_exists, mock_isabs):
self.assertRaises(RuntimeError,
utils.run_ansible_playbook, self.mock_log,
'/tmp', 'existing.yaml', 'localhost,',
'/tmp/foo.cfg'
)
mock_exists.assert_called_once_with('/tmp/foo.cfg')
mock_isabs.assert_called_once_with('/tmp/foo.cfg')
mock_run.assert_not_called()
@mock.patch('tempfile.mkstemp', return_value=('foo', '/tmp/fooBar.cfg'))
@mock.patch('os.path.exists', return_value=True)
@mock.patch('tripleoclient.utils.run_command_and_log')
def test_run_success_default(self, mock_run, mock_exists, mock_mkstemp):
mock_process = mock.Mock()
mock_process.returncode = 0
mock_run.return_value = mock_process
retcode = utils.run_ansible_playbook(self.mock_log,
'/tmp',
'existing.yaml',
'localhost,')
self.assertEqual(retcode, 0)
mock_exists.assert_called_once_with('/tmp/existing.yaml')
env = os.environ.copy()
env['ANSIBLE_CONFIG'] = '/tmp/fooBar.cfg'
mock_run.assert_called_once_with(self.mock_log,
['ansible-playbook', '-i',
'localhost,', '-c', 'smart',
'/tmp/existing.yaml'],
env=env, retcode_only=False)
@mock.patch('os.path.isabs')
@mock.patch('os.path.exists', return_value=True)
@mock.patch('tripleoclient.utils.run_command_and_log')
def test_run_success_ansible_cfg(self, mock_run, mock_exists, mock_isabs):
mock_process = mock.Mock()
mock_process.returncode = 0
mock_run.return_value = mock_process
retcode = utils.run_ansible_playbook(self.mock_log, '/tmp',
'existing.yaml', 'localhost,',
ansible_config='/tmp/foo.cfg')
self.assertEqual(retcode, 0)
mock_isabs.assert_called_once_with('/tmp/foo.cfg')
exist_calls = [mock.call('/tmp/foo.cfg'),
mock.call('/tmp/existing.yaml')]
mock_exists.assert_has_calls(exist_calls, any_order=False)
env = os.environ.copy()
env['ANSIBLE_CONFIG'] = '/tmp/foo.cfg'
mock_run.assert_called_once_with(self.mock_log,
['ansible-playbook', '-i',
'localhost,', '-c', 'smart',
'/tmp/existing.yaml'],
env=env, retcode_only=False)
@mock.patch('tempfile.mkstemp', return_value=('foo', '/tmp/fooBar.cfg'))
@mock.patch('os.path.exists', return_value=True)
@mock.patch('tripleoclient.utils.run_command_and_log')
def test_run_success_connection_local(self, mock_run, mock_exists,
mok_mkstemp):
mock_process = mock.Mock()
mock_process.returncode = 0
mock_run.return_value = mock_process
retcode = utils.run_ansible_playbook(self.mock_log, '/tmp',
'existing.yaml',
'localhost,',
connection='local')
self.assertEqual(retcode, 0)
mock_exists.assert_called_once_with('/tmp/existing.yaml')
env = os.environ.copy()
env['ANSIBLE_CONFIG'] = '/tmp/fooBar.cfg'
mock_run.assert_called_once_with(self.mock_log,
['ansible-playbook', '-i',
'localhost,', '-c', 'local',
'/tmp/existing.yaml'],
env=env, retcode_only=False)
class TestWaitForStackUtil(TestCase):
def setUp(self):
self.mock_orchestration = mock.Mock()

View File

@ -49,6 +49,80 @@ from tripleoclient import constants
from tripleoclient import exceptions
def run_ansible_playbook(logger,
workdir,
playbook,
inventory,
ansible_config=None,
retries=True,
connection='smart',
output_callback='json'):
"""Simple wrapper for ansible-playbook
:param logger: logger instance
:type logger: Logger
:param workdir: location of the playbook
:type workdir: String
:param playbook: playbook filename
:type playbook: String
:param inventory: either proper inventory file, or a coma-separated list
:type inventory: String
:param ansible_config: Pass either Absolute Path, or None to generate a
temporary file, or False to not manage configuration at all
:type ansible_config: String
:param retries: do you want to get a retry_file?
:type retries: Boolean
:param connect: connection type (local, smart, etc)
:type connect: String
:param output_callback: Callback for output format. Defaults to "json"
:type output_callback: String
"""
env = os.environ.copy()
cleanup = False
if ansible_config is None:
_, tmp_config = tempfile.mkstemp(prefix=playbook, suffix='ansible.cfg')
with open(tmp_config, 'w+') as f:
f.write("[defaults]\nstdout_callback = %s\n" % output_callback)
if not retries:
f.write("retry_files_enabled = False\n")
f.close()
env['ANSIBLE_CONFIG'] = tmp_config
cleanup = True
elif os.path.isabs(ansible_config):
if os.path.exists(ansible_config):
env['ANSIBLE_CONFIG'] = ansible_config
else:
raise RuntimeError('No such configuration file: %s' %
ansible_config)
elif os.path.exists(os.path.join(workdir, ansible_config)):
env['ANSIBLE_CONFIG'] = os.path.join(workdir, ansible_config)
play = os.path.join(workdir, playbook)
if os.path.exists(play):
cmd = ['ansible-playbook',
'-i', inventory,
'-c', connection, play
]
proc = run_command_and_log(logger, cmd, env=env, retcode_only=False)
proc.wait()
cleanup and os.unlink(tmp_config)
if proc.returncode != 0:
raise RuntimeError(proc.stdout.read())
return proc.returncode
else:
cleanup and os.unlink(tmp_config)
raise RuntimeError('No such playbook: %s' % play)
def bracket_ipv6(address):
"""Put a bracket around address if it is valid IPv6
@ -1057,27 +1131,36 @@ def bulk_symlink(log, src, dst, tmpd='/tmp'):
shutil.rmtree(tmp, ignore_errors=True)
def run_command_and_log(log, cmd, cwd=None):
def run_command_and_log(log, cmd, cwd=None, env=None, retcode_only=True):
"""Run command and log output
:param log: logger instance for logging
:type log: Logger
:param cmd: command in list form
:param cmd: List
:type cmd: List
:param cwd: current worknig directory for execution
:param cmd: String
:type cmd: String
:param env: modified environment for command run
:type env: List
:param retcode_only: Returns only retcode instead or proc objec
:type retcdode_only: Boolean
"""
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, shell=False,
bufsize=1, cwd=cwd)
bufsize=1, cwd=cwd, env=env)
for line in iter(proc.stdout.readline, b''):
# TODO(aschultz): this should probably goto a log file
log.warning(line.rstrip())
proc.stdout.close()
return proc.wait()
if retcode_only:
for line in iter(proc.stdout.readline, b''):
# TODO(aschultz): this should probably goto a log file
log.warning(line.rstrip())
proc.stdout.close()
return proc.wait()
else:
return proc
def ffwd_upgrade_operator_confirm(parsed_args_yes, log):