Upgrades to error handling, now general try/catch available.

This commit is contained in:
Michael DeHaan 2012-03-13 20:59:05 -04:00
parent 2e1b59a9d2
commit 4ae98ed92d
5 changed files with 92 additions and 52 deletions

View file

@ -198,7 +198,10 @@ if __name__ == '__main__':
(runner, results) = cli.run(options, args)
except AnsibleError as e:
# Generic handler for ansible specific errors
print e
print "ERROR: %s" % str(e)
sys.exit(1)
except Exception as e2:
print e2.__class__
else:
cli.output(runner, results, options, args)

View file

@ -21,6 +21,7 @@
import paramiko
import exceptions
import os
from ansible.errors import *
################################################
@ -39,18 +40,6 @@ class Connection(object):
raise Exception("unsupported connection type")
return conn.connect()
################################################
class AnsibleConnectionException(exceptions.Exception):
''' Subclass of exception for catching in Runner() code '''
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
################################################
# want to implement another connection type?
# follow duck-typing of ParamikoConnection
@ -80,7 +69,7 @@ class ParamikoConnection(object):
timeout=self.runner.timeout
)
except Exception, e:
raise AnsibleConnectionException(str(e))
raise AnsibleConnectionFailed(str(e))
return self
def exec_command(self, cmd):
@ -91,12 +80,12 @@ class ParamikoConnection(object):
def put_file(self, in_path, out_path):
''' transfer a file from local to remote '''
if not os.path.exists(in_path):
raise AnsibleConnectionException("file or module does not exist: %s" % in_path)
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
sftp = self.ssh.open_sftp()
try:
sftp.put(in_path, out_path)
except IOError:
raise AnsibleConnectionException("failed to transfer file to %s" % out_path)
raise AnsibleException("failed to transfer file to %s" % out_path)
sftp.close()
def close(self):

View file

@ -20,18 +20,18 @@ class AnsibleError(Exception):
"""
The base Ansible exception from which all others should subclass.
"""
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class AnsibleFileNotFound(AnsibleError):
pass
class AnsibleConnectionFailed(AnsibleError):
pass
class AnsibleInventoryNotFoundError(AnsibleError):
"""
Exception raised when the default or provided host inventory file
does not exist.
"""
def __init__(self, inventory):
self.inventory = inventory
self.msg = "Unable to continue, inventory file not found: %s" %\
self.inventory
def __str__(self):
return self.msg

View file

@ -18,11 +18,6 @@
################################################
# FIXME: need to add global error handling around
# executor_hook mapping all exceptions into failures
# with the traceback converted into a string and
# if the exception is typed, a *nice* string
try:
import json
except ImportError:
@ -38,15 +33,14 @@ import Queue
import random
import jinja2
import time
from ansible.utils import *
from ansible.errors import AnsibleInventoryNotFoundError
import traceback
# FIXME: stop importing *, use as utils/errors
from ansible.utils import *
from ansible.errors import *
################################################
def noop(*args, **kwargs):
pass
def _executor_hook(job_queue, result_queue):
''' callback used by multiprocessing pool '''
@ -58,6 +52,14 @@ def _executor_hook(job_queue, result_queue):
result_queue.put(runner._executor(host))
except Queue.Empty:
pass
except AnsibleError, ae:
result_queue.put([host, False, str(ae)])
except Exception, ee:
# probably should include the full trace
result_queue.put([host, False, traceback.format_exc()])
################################################
class Runner(object):
@ -116,6 +118,8 @@ class Runner(object):
self.generated_jid = str(random.randint(0, 999999999999))
self.connector = ansible.connection.Connection(self, transport)
# *****************************************************
@classmethod
def parse_hosts(cls, host_list):
'''
@ -131,7 +135,7 @@ class Runner(object):
host_list = os.path.expanduser(host_list)
if not os.path.exists(host_list):
raise AnsibleInventoryNotFoundError(host_list)
raise AnsibleFileNotFound("inventory file not found: %s" % host_list)
lines = file(host_list).read().split("\n")
groups = {}
@ -154,9 +158,11 @@ class Runner(object):
return (results, groups)
# *****************************************************
def _matches(self, host_name, pattern=None):
''' returns if a hostname is matched by the pattern '''
# a pattern is in fnmatch format but more than one pattern
# can be strung together with semicolons. ex:
# atlanta-web*.example.com;dc-web*.example.com
@ -177,19 +183,25 @@ class Runner(object):
return True
return False
# *****************************************************
def _connect(self, host):
'''
obtains a connection to the host.
on success, returns (True, connection)
on failure, returns (False, traceback str)
'''
try:
return [ True, self.connector.connect(host) ]
except ansible.connection.AnsibleConnectionException, e:
except AnsibleConnectionFailed, e:
return [ False, "FAILED: %s" % str(e) ]
# *****************************************************
def _return_from_module(self, conn, host, result):
''' helper function to handle JSON parsing of results '''
try:
# try to parse the JSON response
return [ host, True, json.loads(result) ]
@ -197,8 +209,11 @@ class Runner(object):
# it failed, say so, but return the string anyway
return [ host, False, "%s/%s" % (str(e), result) ]
# *****************************************************
def _delete_remote_files(self, conn, files):
''' deletes one or more remote files '''
if type(files) == str:
files = [ files ]
for filename in files:
@ -206,25 +221,34 @@ class Runner(object):
raise Exception("not going to happen")
self._exec_command(conn, "rm -rf %s" % filename)
# *****************************************************
def _transfer_file(self, conn, source, dest):
''' transfers a remote file '''
self.remote_log(conn, 'COPY remote:%s local:%s' % (source, dest))
conn.put_file(source, dest)
# *****************************************************
def _transfer_module(self, conn, tmp, module):
'''
transfers a module file to the remote side to execute it,
but does not execute it yet
'''
outpath = self._copy_module(conn, tmp, module)
self._exec_command(conn, "chmod +x %s" % outpath)
return outpath
# *****************************************************
def _execute_module(self, conn, tmp, remote_module_path, module_args):
'''
runs a module that has already been transferred, but first
modifies the command using setup_cache variables (see playbook)
'''
args = module_args
if type(args) == list:
args = [ str(x) for x in module_args ]
@ -248,13 +272,15 @@ class Runner(object):
result = self._exec_command(conn, cmd)
return result
# *****************************************************
def _execute_normal_module(self, conn, host, tmp):
'''
transfer & execute a module that is not 'copy' or 'template'
because those require extra work.
'''
module = self._transfer_module(conn, tmp, self.module_name)
module = self._transfer_module(conn, tmp, self.module_name)
result = self._execute_module(conn, tmp, module, self.module_args)
# when running the setup module, which pushes vars to the host and ALSO
@ -270,11 +296,14 @@ class Runner(object):
return self._return_from_module(conn, host, result)
# *****************************************************
def _execute_async_module(self, conn, host, tmp):
'''
transfer the given module name, plus the async module
and then run the async module wrapping the other module
'''
async = self._transfer_module(conn, tmp, 'async_wrapper')
module = self._transfer_module(conn, tmp, self.module_name)
new_args = []
@ -283,8 +312,12 @@ class Runner(object):
result = self._execute_module(conn, tmp, async, new_args)
return self._return_from_module(conn, host, result)
# *****************************************************
def _parse_kv(self, args):
# FIXME: move to utils
''' helper function to convert a string of key/value items to a dict '''
options = {}
for x in args:
if x.find("=") != -1:
@ -292,6 +325,8 @@ class Runner(object):
options[k]=v
return options
# *****************************************************
def _execute_copy(self, conn, host, tmp):
''' handler for file transfer operations '''
@ -314,6 +349,8 @@ class Runner(object):
result = self._execute_module(conn, tmp, module, args)
return self._return_from_module(conn, host, result)
# *****************************************************
def _execute_template(self, conn, host, tmp):
''' handler for template operations '''
@ -343,6 +380,7 @@ class Runner(object):
result = self._execute_module(conn, tmp, template_module, args)
return self._return_from_module(conn, host, result)
# *****************************************************
def _executor(self, host):
'''
@ -382,47 +420,57 @@ class Runner(object):
return result
# *****************************************************
def remote_log(self, conn, msg):
''' this is the function we use to log things '''
# FIXME: TODO: make this optional as it's executed a lot
stdin, stdout, stderr = conn.exec_command('/usr/bin/logger -t ansible -p auth.info "%s"' % msg)
# *****************************************************
def _exec_command(self, conn, cmd):
''' execute a command string over SSH, return the output '''
msg = '%s: %s' % (self.module_name, cmd)
self.remote_log(conn, msg)
stdin, stdout, stderr = conn.exec_command(cmd)
results = "\n".join(stdout.readlines())
return results
# *****************************************************
def _get_tmp_path(self, conn):
''' gets a temporary path on a remote box '''
result = self._exec_command(conn, "mktemp -d /tmp/ansible.XXXXXX")
return result.split("\n")[0] + '/'
# *****************************************************
def _copy_module(self, conn, tmp, module):
''' transfer a module over SFTP, does not run it '''
if module.startswith("/"):
# user probably did "/bin/foo" instead of "command /bin/foo" in a playbook
# or tried "-m /bin/foo" instead of "a /bin/foo"
# FIXME: type this exception
raise Exception("%s is not a module" % module)
in_path = os.path.expanduser(
os.path.join(self.module_path, module)
)
raise AnsibleFileNotFound("%s is not a module" % module)
in_path = os.path.expanduser(os.path.join(self.module_path, module))
if not os.path.exists(in_path):
# FIXME: type this exception
raise Exception("module not found: %s" % in_path)
raise AnsibleFileNotFound("module not found: %s" % in_path)
out_path = tmp + module
conn.put_file(in_path, out_path)
return out_path
# *****************************************************
def match_hosts(self, pattern):
''' return all matched hosts fitting a pattern '''
return [ h for h in self.host_list if self._matches(h, pattern) ]
# *****************************************************
def run(self):
''' xfer & run module on all matched hosts '''

View file

@ -136,7 +136,7 @@ def dark_hosts_msg(results):
''' summarize the results of all uncontactable hosts '''
buf = ''
if len(results['dark'].keys()) > 0:
buf += "\n*** Hosts which could not be contacted or did not respond: ***\n"
buf += "\n*** Hosts with fatal errors: ***\n"
for hostname in results['dark'].keys():
buf += "%s: %s\n" % (hostname, results['dark'][hostname])
buf += "\n"