Upgrades to error handling, now general try/catch available.
This commit is contained in:
parent
2e1b59a9d2
commit
4ae98ed92d
5 changed files with 92 additions and 52 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 '''
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue