diff --git a/lib/ansible/connection.py b/lib/ansible/connection.py new file mode 100755 index 0000000000..8f57f61aee --- /dev/null +++ b/lib/ansible/connection.py @@ -0,0 +1,106 @@ +# (c) 2012, Michael DeHaan +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +################################################ + +import paramiko +import exceptions + +################################################ + +class Connection(object): + ''' Handles abstract connections to remote hosts ''' + + def __init__(self, runner, transport): + self.runner = runner + self.transport = transport + + def connect(self, host): + conn = None + if self.transport == 'paramiko': + conn = ParamikoConnection(self.runner, host) + if conn is None: + 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 +# you may wish to read config files in __init__ +# if you have any. Paramiko does not need any. + +class ParamikoConnection(object): + ''' SSH based connections with Paramiko ''' + + def __init__(self, runner, host): + self.ssh = None + self.runner = runner + self.host = host + + def connect(self): + ''' connect to the remote host ''' + + self.ssh = paramiko.SSHClient() + self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + self.ssh.connect( + self.host, + username=self.runner.remote_user, + allow_agent=True, + look_for_keys=True, + password=self.runner.remote_pass, + timeout=self.runner.timeout + ) + except Exception, e: + raise AnsibleConnectionException(str(e)) + return self + + def exec_command(self, cmd): + ''' run a command on the remote host ''' + + stdin, stdout, stderr = self.ssh.exec_command(cmd) + return (stdin, stdout, stderr) + + def put_file(self, in_path, out_path): + ''' transfer a file from local to remote ''' + + sftp = self.ssh.open_sftp() + sftp.put(in_path, out_path) + sftp.close() + + def close(self): + ''' terminate the connection ''' + + self.ssh.close() + +############################################ +# add other connection types here + + diff --git a/lib/ansible/runner.py b/lib/ansible/runner.py index 1c9ef89caf..4bf3aae62e 100755 --- a/lib/ansible/runner.py +++ b/lib/ansible/runner.py @@ -28,9 +28,9 @@ import multiprocessing import signal import os import ansible.constants as C +import ansible.connection import Queue import random -import paramiko import jinja2 from ansible.utils import * @@ -66,6 +66,7 @@ class Runner(object): background=0, basedir=None, setup_cache={}, + transport='paramiko', verbose=False): ''' @@ -105,6 +106,7 @@ class Runner(object): random.seed() self.generated_jid = str(random.randint(0, 999999999999)) + self.connector = ansible.connection.Connection(self, transport) @classmethod def parse_hosts(cls, host_list): @@ -159,19 +161,13 @@ class Runner(object): def _connect(self, host): ''' - obtains a paramiko connection to the host. + obtains a connection to the host. on success, returns (True, connection) on failure, returns (False, traceback str) ''' - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - # try paramiko - ssh.connect(host, username=self.remote_user, allow_agent=True, - look_for_keys=True, password=self.remote_pass, timeout=self.timeout) - return [ True, ssh ] - except Exception, e: - # it failed somehow, return the failure string + return [ True, self.connector.connect(host) ] + except ansible.connection.AnsibleConnectionException, e: return [ False, "FAILED: %s" % str(e) ] def _return_from_module(self, conn, host, result): @@ -195,9 +191,7 @@ class Runner(object): def _transfer_file(self, conn, source, dest): ''' transfers a remote file ''' self.remote_log(conn, 'COPY remote:%s local:%s' % (source, dest)) - sftp = conn.open_sftp() - sftp.put(source, dest) - sftp.close() + conn.put_file(source, dest) def _transfer_module(self, conn, tmp, module): ''' @@ -217,7 +211,7 @@ class Runner(object): if type(args) == list: args = [ str(x) for x in module_args ] args = " ".join(args) - inject_vars = self.setup_cache.get(conn._host,{}) + inject_vars = self.setup_cache.get(conn.host,{}) # the metadata location for the setup module is transparently managed # since it's an 'internals' module, kind of a black box. See playbook @@ -250,7 +244,7 @@ class Runner(object): # run AFTER setup use these variables for templating when executed # from playbooks if self.module_name == 'setup': - host = conn._host + host = conn.host try: var_result = json.loads(result) except: @@ -351,7 +345,6 @@ class Runner(object): if not ok: return [ host, False, conn ] - conn._host = host tmp = self._get_tmp_path(conn) result = None if self.module_name not in [ 'copy', 'template' ]: @@ -397,9 +390,7 @@ class Runner(object): os.path.join(self.module_path, module) ) out_path = tmp + module - sftp = conn.open_sftp() - sftp.put(in_path, out_path) - sftp.close() + conn.put_file(in_path, out_path) return out_path def match_hosts(self, pattern): diff --git a/lib/ansible/utils.py b/lib/ansible/utils.py index 29f37bdd22..1a39e9d7f5 100755 --- a/lib/ansible/utils.py +++ b/lib/ansible/utils.py @@ -54,9 +54,9 @@ def task_start_msg(name, conditional): def regular_generic_msg(hostname, result, oneline, caption): ''' output on the result of a module run that is not command ''' if not oneline: - return "%s | %s >>\n%s" % (hostname, caption, bigjson(result)) + return "%s | %s >> %s\n" % (hostname, caption, bigjson(result)) else: - return "%s | %s >> %s" % (hostname, caption, smjson(result)) + return "%s | %s >> %s\n" % (hostname, caption, smjson(result)) def regular_success_msg(hostname, result, oneline): ''' output the result of a successful module run ''' @@ -135,9 +135,9 @@ def dark_hosts_msg(results): ''' summarize the results of all uncontactable hosts ''' buf = '' if len(results['dark'].keys()) > 0: - buf += "*** Hosts which could not be contacted or did not respond: ***" + buf += "\n*** Hosts which could not be contacted or did not respond: ***\n" for hostname in results['dark'].keys(): - buf += "%s:\n%s\n" % (hostname, results['dark'][hostname]) + buf += "%s: %s\n" % (hostname, results['dark'][hostname]) buf += "\n" return buf