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

pull/70/head
Michael DeHaan 13 years ago
parent 2e1b59a9d2
commit 4ae98ed92d

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

@ -21,6 +21,7 @@
import paramiko import paramiko
import exceptions import exceptions
import os import os
from ansible.errors import *
################################################ ################################################
@ -39,18 +40,6 @@ class Connection(object):
raise Exception("unsupported connection type") raise Exception("unsupported connection type")
return conn.connect() 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? # want to implement another connection type?
# follow duck-typing of ParamikoConnection # follow duck-typing of ParamikoConnection
@ -80,7 +69,7 @@ class ParamikoConnection(object):
timeout=self.runner.timeout timeout=self.runner.timeout
) )
except Exception, e: except Exception, e:
raise AnsibleConnectionException(str(e)) raise AnsibleConnectionFailed(str(e))
return self return self
def exec_command(self, cmd): def exec_command(self, cmd):
@ -91,12 +80,12 @@ class ParamikoConnection(object):
def put_file(self, in_path, out_path): def put_file(self, in_path, out_path):
''' transfer a file from local to remote ''' ''' transfer a file from local to remote '''
if not os.path.exists(in_path): 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() sftp = self.ssh.open_sftp()
try: try:
sftp.put(in_path, out_path) sftp.put(in_path, out_path)
except IOError: except IOError:
raise AnsibleConnectionException("failed to transfer file to %s" % out_path) raise AnsibleException("failed to transfer file to %s" % out_path)
sftp.close() sftp.close()
def close(self): def close(self):

@ -20,18 +20,18 @@ class AnsibleError(Exception):
""" """
The base Ansible exception from which all others should subclass. The base Ansible exception from which all others should subclass.
""" """
pass
class AnsibleInventoryNotFoundError(AnsibleError): def __init__(self, msg):
""" self.msg = msg
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): def __str__(self):
return self.msg return self.msg
class AnsibleFileNotFound(AnsibleError):
pass
class AnsibleConnectionFailed(AnsibleError):
pass

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

@ -136,7 +136,7 @@ def dark_hosts_msg(results):
''' summarize the results of all uncontactable hosts ''' ''' summarize the results of all uncontactable hosts '''
buf = '' buf = ''
if len(results['dark'].keys()) > 0: 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(): for hostname in results['dark'].keys():
buf += "%s: %s\n" % (hostname, results['dark'][hostname]) buf += "%s: %s\n" % (hostname, results['dark'][hostname])
buf += "\n" buf += "\n"

Loading…
Cancel
Save