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:
parent
1e27b1e571
commit
edc7365c90
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue