docker_service: Forward stderr-based output from docker-compose (#25456)

PR #5165 at https://github.com/ansible/ansible-modules-core/pull/5165
adds redirection and capture of stdout during execution of
docker-compose.

This doesn't necessarily catch all errors, since some are printed to
stderr and lost.

This extends the redirection to include stderr, and does minor string
processing to attempt to find a 'useful' message to present as the
final Ansible error.
This commit is contained in:
Chris Houseknecht 2017-06-07 23:30:26 -04:00 committed by GitHub
parent 13c070948b
commit 20bddde669
2 changed files with 102 additions and 41 deletions

142
lib/ansible/modules/cloud/docker/docker_service.py Normal file → Executable file
View file

@ -503,20 +503,78 @@ def stdout_redirector(path_name):
finally: finally:
sys.stdout = old_stdout sys.stdout = old_stdout
def get_stdout(path_name):
full_stdout = '' @contextmanager
last_line = '' def stderr_redirector(path_name):
old_fh = sys.stderr
fd = open(path_name, 'w')
sys.stderr = fd
try:
yield
finally:
sys.stderr = old_fh
def make_redirection_tempfiles():
_, out_redir_name = tempfile.mkstemp(prefix="ansible")
_, err_redir_name = tempfile.mkstemp(prefix="ansible")
return (out_redir_name, err_redir_name)
def cleanup_redirection_tempfiles(out_name, err_name):
get_redirected_output(out_name)
get_redirected_output(err_name)
def get_redirected_output(path_name):
output = []
with open(path_name, 'r') as fd: with open(path_name, 'r') as fd:
for line in fd: for line in fd:
# strip terminal format/color chars # strip terminal format/color chars
new_line = re.sub(r'\x1b\[.+m', '', line.encode('ascii')) new_line = re.sub(r'\x1b\[.+m', '', line.encode('ascii'))
full_stdout += new_line output.append(new_line)
if new_line.strip():
# Assuming last line contains the error message
last_line = new_line.strip().encode('utf-8')
fd.close() fd.close()
os.remove(path_name) os.remove(path_name)
return full_stdout, last_line return output
def attempt_extract_errors(exc_str, stdout, stderr):
errors = [l.strip() for l in stderr if l.strip().startswith('ERROR:')]
errors.extend([l.strip() for l in stdout if l.strip().startswith('ERROR:')])
warnings = [l.strip() for l in stderr if l.strip().startswith('WARNING:')]
warnings.extend([l.strip() for l in stdout if l.strip().startswith('WARNING:')])
# assume either the exception body (if present) or the last warning was the 'most'
# fatal.
if exc_str.strip():
msg = exc_str.strip()
elif errors:
msg = errors[-1].encode('utf-8')
else:
msg = 'unknown cause'
return {
'warnings': [w.encode('utf-8') for w in warnings],
'errors': [e.encode('utf-8') for e in errors],
'msg': msg,
'module_stderr': ''.join(stderr),
'module_stdout': ''.join(stdout)
}
def get_failure_info(exc, out_name, err_name=None, msg_format='%s'):
if err_name is None:
stderr = []
else:
stderr = get_redirected_output(err_name)
stdout = get_redirected_output(out_name)
reason = attempt_extract_errors(str(exc), stdout, stderr)
reason['msg'] = msg_format % reason['msg']
return reason
class ContainerManager(DockerBaseClass): class ContainerManager(DockerBaseClass):
@ -565,7 +623,8 @@ class ContainerManager(DockerBaseClass):
self.options[u'--file'] = self.files self.options[u'--file'] = self.files
if not HAS_COMPOSE: if not HAS_COMPOSE:
self.client.fail("Unable to load docker-compose. Try `pip install docker-compose`. Error: %s" % HAS_COMPOSE_EXC) self.client.fail("Unable to load docker-compose. Try `pip install docker-compose`. Error: %s" %
HAS_COMPOSE_EXC)
if LooseVersion(compose_version) < LooseVersion(MINIMUM_COMPOSE_VERSION): if LooseVersion(compose_version) < LooseVersion(MINIMUM_COMPOSE_VERSION):
self.client.fail("Found docker-compose version %s. Minimum required version is %s. " self.client.fail("Found docker-compose version %s. Minimum required version is %s. "
@ -682,25 +741,26 @@ class ContainerManager(DockerBaseClass):
result['actions'].append(result_action) result['actions'].append(result_action)
if not self.check_mode and result['changed']: if not self.check_mode and result['changed']:
_, fd_name = tempfile.mkstemp(prefix="ansible") out_redir_name, err_redir_name = make_redirection_tempfiles()
try: try:
with stdout_redirector(fd_name): with stdout_redirector(out_redir_name):
do_build = build_action_from_opts(up_options) with stderr_redirector(err_redir_name):
self.log('Setting do_build to %s' % do_build) do_build = build_action_from_opts(up_options)
self.project.up( self.log('Setting do_build to %s' % do_build)
service_names=service_names, self.project.up(
start_deps=start_deps, service_names=service_names,
strategy=converge, start_deps=start_deps,
do_build=do_build, strategy=converge,
detached=detached, do_build=do_build,
remove_orphans=self.remove_orphans, detached=detached,
timeout=self.timeout) remove_orphans=self.remove_orphans,
timeout=self.timeout)
except Exception as exc: except Exception as exc:
full_stdout, last_line= get_stdout(fd_name) fail_reason = get_failure_info(exc, out_redir_name, err_redir_name,
self.client.module.fail_json(msg="Error starting project %s" % str(exc), module_stderr=last_line, msg_format="Error starting project %s")
module_stdout=full_stdout) self.client.module.fail_json(**fail_reason)
else: else:
get_stdout(fd_name) cleanup_redirection_tempfiles(out_redir_name, err_redir_name)
if self.stopped: if self.stopped:
stop_output = self.cmd_stop(service_names) stop_output = self.cmd_stop(service_names)
@ -903,16 +963,17 @@ class ContainerManager(DockerBaseClass):
)) ))
result['actions'].append(service_res) result['actions'].append(service_res)
if not self.check_mode and result['changed']: if not self.check_mode and result['changed']:
_, fd_name = tempfile.mkstemp(prefix="ansible") out_redir_name, err_redir_name = make_redirection_tempfiles()
try: try:
with stdout_redirector(fd_name): with stdout_redirector(out_redir_name):
self.project.stop(service_names=service_names, timeout=self.timeout) with stderr_redirector(err_redir_name):
self.project.stop(service_names=service_names, timeout=self.timeout)
except Exception as exc: except Exception as exc:
full_stdout, last_line = get_stdout(fd_name) fail_reason = get_failure_info(exc, out_redir_name, err_redir_name,
self.client.module.fail_json(msg="Error stopping project %s" % str(exc), module_stderr=last_line, msg_format="Error stopping project %s")
module_stdout=full_stdout) self.client.module.fail_json(**fail_reason)
else: else:
get_stdout(fd_name) cleanup_redirection_tempfiles(out_redir_name, err_redir_name)
return result return result
def cmd_restart(self, service_names): def cmd_restart(self, service_names):
@ -937,16 +998,17 @@ class ContainerManager(DockerBaseClass):
result['actions'].append(service_res) result['actions'].append(service_res)
if not self.check_mode and result['changed']: if not self.check_mode and result['changed']:
_, fd_name = tempfile.mkstemp(prefix="ansible") out_redir_name, err_redir_name = make_redirection_tempfiles()
try: try:
with stdout_redirector(fd_name): with stdout_redirector(out_redir_name):
self.project.restart(service_names=service_names, timeout=self.timeout) with stderr_redirector(err_redir_name):
self.project.restart(service_names=service_names, timeout=self.timeout)
except Exception as exc: except Exception as exc:
full_stdout, last_line = get_stdout(fd_name) fail_reason = get_failure_info(exc, out_redir_name, err_redir_name,
self.client.module.fail_json(msg="Error restarting project %s" % str(exc), module_stderr=last_line, msg_format="Error restarting project %s")
module_stdout=full_stdout) self.client.module.fail_json(**fail_reason)
else: else:
get_stdout(fd_name) cleanup_redirection_tempfiles(out_redir_name, err_redir_name)
return result return result
def cmd_scale(self): def cmd_scale(self):
@ -981,7 +1043,7 @@ def main():
state=dict(type='str', choices=['absent', 'present'], default='present'), state=dict(type='str', choices=['absent', 'present'], default='present'),
definition=dict(type='dict'), definition=dict(type='dict'),
hostname_check=dict(type='bool', default=False), hostname_check=dict(type='bool', default=False),
recreate=dict(type='str', choices=['always','never','smart'], default='smart'), recreate=dict(type='str', choices=['always', 'never', 'smart'], default='smart'),
build=dict(type='bool', default=False), build=dict(type='bool', default=False),
remove_images=dict(type='str', choices=['all', 'local']), remove_images=dict(type='str', choices=['all', 'local']),
remove_volumes=dict(type='bool', default=False), remove_volumes=dict(type='bool', default=False),

View file

@ -108,7 +108,6 @@ lib/ansible/modules/cloud/docker/docker_image.py
lib/ansible/modules/cloud/docker/docker_image_facts.py lib/ansible/modules/cloud/docker/docker_image_facts.py
lib/ansible/modules/cloud/docker/docker_login.py lib/ansible/modules/cloud/docker/docker_login.py
lib/ansible/modules/cloud/docker/docker_network.py lib/ansible/modules/cloud/docker/docker_network.py
lib/ansible/modules/cloud/docker/docker_service.py
lib/ansible/modules/cloud/google/gc_storage.py lib/ansible/modules/cloud/google/gc_storage.py
lib/ansible/modules/cloud/google/gcdns_record.py lib/ansible/modules/cloud/google/gcdns_record.py
lib/ansible/modules/cloud/google/gcdns_zone.py lib/ansible/modules/cloud/google/gcdns_zone.py