Improve the API for connection plugins and update local and ssh to use it

This commit is contained in:
Toshio Kuratomi 2015-04-15 16:32:44 -07:00
parent 1f7d23fc18
commit 01df51d2ae
5 changed files with 126 additions and 72 deletions

View file

@ -374,8 +374,6 @@ class TaskExecutor:
if not connection:
raise AnsibleError("the connection plugin '%s' was not found" % conn_type)
connection.connect()
return connection
def _get_action_handler(self, connection):

View file

@ -168,7 +168,7 @@ class ActionBase:
if result['rc'] != 0:
if result['rc'] == 5:
output = 'Authentication failure.'
elif result['rc'] == 255 and self._connection.get_transport() in ['ssh']:
elif result['rc'] == 255 and self._connection.transport in ('ssh',):
# FIXME: more utils.VERBOSITY
#if utils.VERBOSITY > 3:
# output = 'SSH encountered an unknown error. The output was:\n%s' % (result['stdout']+result['stderr'])

View file

@ -1,4 +1,5 @@
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
#
# This file is part of Ansible
#
@ -19,6 +20,10 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from abc import ABCMeta, abstractmethod, abstractproperty
from six import add_metaclass
from ansible import constants as C
from ansible.errors import AnsibleError
@ -29,7 +34,7 @@ from ansible.utils.display import Display
__all__ = ['ConnectionBase']
@add_metaclass(ABCMeta)
class ConnectionBase:
'''
A base class for connections to contain common code.
@ -39,9 +44,15 @@ class ConnectionBase:
become_methods = C.BECOME_METHODS
def __init__(self, connection_info, *args, **kwargs):
self._connection_info = connection_info
self._display = Display(verbosity=connection_info.verbosity)
# All these hasattrs allow subclasses to override these parameters
if not hasattr(self, '_connection_info'):
self._connection_info = connection_info
if not hasattr(self, '_display'):
self._display = Display(verbosity=connection_info.verbosity)
if not hasattr(self, '_connected'):
self._connected = False
self._connect()
def _become_method_supported(self, become_method):
''' Checks if the current class supports this privilege escalation method '''
@ -50,3 +61,33 @@ class ConnectionBase:
return True
raise AnsibleError("Internal Error: this connection module does not support running commands via %s" % become_method)
@abstractproperty
def transport(self):
"""String used to identify this Connection class from other classes"""
pass
@abstractmethod
def _connect(self):
"""Connect to the host we've been initialized with"""
pass
@abstractmethod
def exec_command(self, cmd, tmp_path, executable=None, in_data=None):
"""Run a command on the remote host"""
pass
@abstractmethod
def put_file(self, in_path, out_path):
"""Transfer a file from local to remote"""
pass
@abstractmethod
def fetch_file(self, in_path, out_path):
"""Fetch a file from remote to local"""
pass
@abstractmethod
def close(self):
"""Terminate the connection"""
pass

View file

@ -1,4 +1,5 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
#
# This file is part of Ansible
#
@ -19,13 +20,12 @@ __metaclass__ = type
import traceback
import os
import pipes
import shutil
import subprocess
import select
import fcntl
#import select
#import fcntl
from ansible.errors import AnsibleError
from ansible.errors import AnsibleError, AnsibleFileNotFound
from ansible.plugins.connections import ConnectionBase
from ansible.utils.debug import debug
@ -33,15 +33,17 @@ from ansible.utils.debug import debug
class Connection(ConnectionBase):
''' Local based connections '''
def get_transport(self):
@property
def transport(self):
''' used to identify this connection object '''
return 'local'
def connect(self, port=None):
def _connect(self, port=None):
''' connect to the local host; nothing to do here '''
self._display.vvv("ESTABLISH LOCAL CONNECTION FOR USER: %s" % self._connection_info.remote_user, host=self._connection_info.remote_addr)
if not self._connected:
self._display.vvv("ESTABLISH LOCAL CONNECTION FOR USER: {0}".format(self._connection_info.remote_user, host=self._connection_info.remote_addr))
self._connected = True
return self
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None):
@ -57,7 +59,7 @@ class Connection(ConnectionBase):
executable = executable.split()[0] if executable else None
self._display.vvv("%s EXEC %s" % (self._connection_info.remote_addr, cmd))
self._display.vvv("{0} EXEC {1}".format(self._connection_info.remote_addr, cmd))
# FIXME: cwd= needs to be set to the basedir of the playbook
debug("opening command with Popen()")
p = subprocess.Popen(
@ -106,26 +108,25 @@ class Connection(ConnectionBase):
def put_file(self, in_path, out_path):
''' transfer a file from local to local '''
#vvv("PUT %s TO %s" % (in_path, out_path), host=self.host)
self._display.vvv("%s PUT %s TO %s" % (self._connection_info.remote_addr, in_path, out_path))
#vvv("PUT {0} TO {1}".format(in_path, out_path), host=self.host)
self._display.vvv("{0} PUT {1} TO {2}".format(self._connection_info.remote_addr, in_path, out_path))
if not os.path.exists(in_path):
#raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
raise AnsibleError("file or module does not exist: %s" % in_path)
raise AnsibleFileNotFound("file or module does not exist: {0}".format(in_path))
try:
shutil.copyfile(in_path, out_path)
except shutil.Error:
traceback.print_exc()
raise AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
raise AnsibleError("failed to copy: {0} and {1} are the same".format(in_path, out_path))
except IOError:
traceback.print_exc()
raise AnsibleError("failed to transfer file to %s" % out_path)
raise AnsibleError("failed to transfer file to {0}".format(out_path))
def fetch_file(self, in_path, out_path):
#vvv("FETCH %s TO %s" % (in_path, out_path), host=self.host)
self._display.vvv("%s FETCH %s TO %s" % (self._connection_info.remote_addr, in_path, out_path))
''' fetch a file from local to local -- for copatibility '''
#vvv("FETCH {0} TO {1}".format(in_path, out_path), host=self.host)
self._display.vvv("{0} FETCH {1} TO {2}".format(self._connection_info.remote_addr, in_path, out_path))
self.put_file(in_path, out_path)
def close(self):
''' terminate the connection; nothing to do here '''
pass
self._connected = False

View file

@ -33,15 +33,13 @@ import pty
from hashlib import sha1
from ansible import constants as C
from ansible.errors import AnsibleError, AnsibleConnectionFailure
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
from ansible.plugins.connections import ConnectionBase
class Connection(ConnectionBase):
''' ssh based connections '''
def __init__(self, connection_info, *args, **kwargs):
super(Connection, self).__init__(connection_info)
# SSH connection specific init stuff
self.HASHED_KEY_MAGIC = "|1|"
self._has_pipelining = True
@ -52,14 +50,20 @@ class Connection(ConnectionBase):
self._cp_dir = '/tmp'
#fcntl.lockf(self.runner.process_lockfile, fcntl.LOCK_UN)
def get_transport(self):
super(Connection, self).__init__(connection_info)
@property
def transport(self):
''' used to identify this connection object from other classes '''
return 'ssh'
def connect(self):
def _connect(self):
''' connect to the remote host '''
self._display.vvv("ESTABLISH SSH CONNECTION FOR USER: %s" % self._connection_info.remote_user, host=self._connection_info.remote_addr)
self._display.vvv("ESTABLISH SSH CONNECTION FOR USER: {0}".format(self._connection_info.remote_user), host=self._connection_info.remote_addr)
if self._connected:
return self
self._common_args = []
extra_args = C.ANSIBLE_SSH_ARGS
@ -67,11 +71,11 @@ class Connection(ConnectionBase):
# make sure there is no empty string added as this can produce weird errors
self._common_args += [x.strip() for x in shlex.split(extra_args) if x.strip()]
else:
self._common_args += [
self._common_args += (
"-o", "ControlMaster=auto",
"-o", "ControlPersist=60s",
"-o", "ControlPath=\"%s\"" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self._cp_dir)),
]
"-o", "ControlPath=\"{0}\"".format(C.ANSIBLE_SSH_CONTROL_PATH.format(dict(directory=self._cp_dir))),
)
cp_in_use = False
cp_path_set = False
@ -82,30 +86,34 @@ class Connection(ConnectionBase):
cp_path_set = True
if cp_in_use and not cp_path_set:
self._common_args += ["-o", "ControlPath=\"%s\"" % (C.ANSIBLE_SSH_CONTROL_PATH % dict(directory=self._cp_dir))]
self._common_args += ("-o", "ControlPath=\"{0}\"".format(
C.ANSIBLE_SSH_CONTROL_PATH.format(dict(directory=self._cp_dir)))
)
if not C.HOST_KEY_CHECKING:
self._common_args += ["-o", "StrictHostKeyChecking=no"]
self._common_args += ("-o", "StrictHostKeyChecking=no")
if self._connection_info.port is not None:
self._common_args += ["-o", "Port=%d" % (self._connection_info.port)]
self._common_args += ("-o", "Port={0}".format(self._connection_info.port))
# FIXME: need to get this from connection info
#if self.private_key_file is not None:
# self._common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.private_key_file)]
# self._common_args += ("-o", "IdentityFile=\"{0}\"".format(os.path.expanduser(self.private_key_file)))
#elif self.runner.private_key_file is not None:
# self._common_args += ["-o", "IdentityFile=\"%s\"" % os.path.expanduser(self.runner.private_key_file)]
# self._common_args += ("-o", "IdentityFile=\"{0}\"".format(os.path.expanduser(self.runner.private_key_file)))
if self._connection_info.password:
self._common_args += ["-o", "GSSAPIAuthentication=no",
"-o", "PubkeyAuthentication=no"]
self._common_args += ("-o", "GSSAPIAuthentication=no",
"-o", "PubkeyAuthentication=no")
else:
self._common_args += ["-o", "KbdInteractiveAuthentication=no",
self._common_args += ("-o", "KbdInteractiveAuthentication=no",
"-o", "PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey",
"-o", "PasswordAuthentication=no"]
"-o", "PasswordAuthentication=no")
if self._connection_info.remote_user is not None and self._connection_info.remote_user != pwd.getpwuid(os.geteuid())[0]:
self._common_args += ["-o", "User="+self._connection_info.remote_user]
self._common_args += ("-o", "User={0}".format(self._connection_info.remote_user))
# FIXME: figure out where this goes
#self._common_args += ["-o", "ConnectTimeout=%d" % self.runner.timeout]
self._common_args += ["-o", "ConnectTimeout=15"]
#self._common_args += ("-o", "ConnectTimeout={0}".format(self.runner.timeout))
self._common_args += ("-o", "ConnectTimeout=15")
self._connected = True
return self
@ -136,13 +144,13 @@ class Connection(ConnectionBase):
except OSError:
raise AnsibleError("to use the 'ssh' connection type with passwords, you must install the sshpass program")
(self.rfd, self.wfd) = os.pipe()
return ["sshpass", "-d%d" % self.rfd]
return ("sshpass", "-d{0}".format(self.rfd))
return []
def _send_password(self):
if self._connection_info.password:
os.close(self.rfd)
os.write(self.wfd, "%s\n" % self._connection_info.password)
os.write(self.wfd, "{0}\n".format(self._connection_info.password))
os.close(self.wfd)
def _communicate(self, p, stdin, indata, su=False, sudoable=False, prompt=None):
@ -215,12 +223,12 @@ class Connection(ConnectionBase):
else:
user_host_file = "~/.ssh/known_hosts"
user_host_file = os.path.expanduser(user_host_file)
host_file_list = []
host_file_list.append(user_host_file)
host_file_list.append("/etc/ssh/ssh_known_hosts")
host_file_list.append("/etc/ssh/ssh_known_hosts2")
hfiles_not_found = 0
for hf in host_file_list:
if not os.path.exists(hf):
@ -234,7 +242,7 @@ class Connection(ConnectionBase):
else:
data = host_fh.read()
host_fh.close()
for line in data.split("\n"):
if line is None or " " not in line:
continue
@ -258,33 +266,33 @@ class Connection(ConnectionBase):
return False
if (hfiles_not_found == len(host_file_list)):
self._display.vvv("EXEC previous known host file not found for %s" % host)
self._display.vvv("EXEC previous known host file not found for {0}".format(host))
return True
def exec_command(self, cmd, tmp_path, executable='/bin/sh', in_data=None):
''' run a command on the remote host '''
ssh_cmd = self._password_cmd()
ssh_cmd += ["ssh", "-C"]
ssh_cmd += ("ssh", "-C")
if not in_data:
# we can only use tty when we are not pipelining the modules. piping data into /usr/bin/python
# inside a tty automatically invokes the python interactive-mode but the modules are not
# compatible with the interactive-mode ("unexpected indent" mainly because of empty lines)
ssh_cmd += ["-tt"]
ssh_cmd.append("-tt")
if self._connection_info.verbosity > 3:
ssh_cmd += ["-vvv"]
ssh_cmd.append("-vvv")
else:
ssh_cmd += ["-q"]
ssh_cmd.append("-q")
ssh_cmd += self._common_args
# FIXME: ipv6 stuff needs to be figured out. It's in the connection info, however
# not sure if it's all working yet so this remains commented out
#if self._ipv6:
# ssh_cmd += ['-6']
ssh_cmd += [self._connection_info.remote_addr]
ssh_cmd.append(self._connection_info.remote_addr)
ssh_cmd.append(cmd)
self._display.vvv("EXEC %s" % ' '.join(ssh_cmd), host=self._connection_info.remote_addr)
self._display.vvv("EXEC {0}".format(' '.join(ssh_cmd)), host=self._connection_info.remote_addr)
not_in_host_file = self.not_in_host_file(self._connection_info.remote_addr)
@ -361,7 +369,7 @@ class Connection(ConnectionBase):
# FIXME: the prompt won't be here anymore
prompt=""
(returncode, stdout, stderr) = self._communicate(p, stdin, in_data, prompt=prompt)
#if C.HOST_KEY_CHECKING and not_in_host_file:
# # lock around the initial SSH connectivity so the user prompt about whether to add
# # the host to known hosts is not intermingled with multiprocess output.
@ -384,9 +392,9 @@ class Connection(ConnectionBase):
def put_file(self, in_path, out_path):
''' transfer a file from local to remote '''
self._display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
self._display.vvv("PUT {0} TO {1}".format(in_path, out_path), host=self._connection_info.remote_addr)
if not os.path.exists(in_path):
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
raise AnsibleFileNotFound("file or module does not exist: {0}".format(in_path))
cmd = self._password_cmd()
# FIXME: make a function, used in all 3 methods EXEC/PUT/FETCH
@ -398,12 +406,15 @@ class Connection(ConnectionBase):
# host = '[%s]' % host
if C.DEFAULT_SCP_IF_SSH:
cmd += ["scp"] + self._common_args
cmd += [in_path,host + ":" + pipes.quote(out_path)]
cmd.append('scp')
cmd += self._common_args
cmd.append(in_path,host + ":" + pipes.quote(out_path))
indata = None
else:
cmd += ["sftp"] + self._common_args + [host]
indata = "put %s %s\n" % (pipes.quote(in_path), pipes.quote(out_path))
cmd.append('sftp')
cmd += self._common_args
cmd.append(host)
indata = "put {0} {1}\n".format(pipes.quote(in_path), pipes.quote(out_path))
(p, stdin) = self._run(cmd, indata)
@ -412,11 +423,11 @@ class Connection(ConnectionBase):
(returncode, stdout, stderr) = self._communicate(p, stdin, indata)
if returncode != 0:
raise AnsibleError("failed to transfer file to %s:\n%s\n%s" % (out_path, stdout, stderr))
raise AnsibleError("failed to transfer file to {0}:\n{1}\n{2}".format(out_path, stdout, stderr))
def fetch_file(self, in_path, out_path):
''' fetch a file from remote to local '''
self._display.vvv("FETCH %s TO %s" % (in_path, out_path), host=self._connection_info.remote_addr)
self._display.vvv("FETCH {0} TO {1}".format(in_path, out_path), host=self._connection_info.remote_addr)
cmd = self._password_cmd()
# FIXME: make a function, used in all 3 methods EXEC/PUT/FETCH
@ -428,21 +439,24 @@ class Connection(ConnectionBase):
# host = '[%s]' % self._connection_info.remote_addr
if C.DEFAULT_SCP_IF_SSH:
cmd += ["scp"] + self._common_args
cmd += [host + ":" + in_path, out_path]
cmd.append('scp')
cmd += self._common_args
cmd += ('{0}:{1}'.format(host, in_path), out_path)
indata = None
else:
cmd += ["sftp"] + self._common_args + [host]
indata = "get %s %s\n" % (in_path, out_path)
cmd.append('sftp')
cmd += self._common_args
cmd.append(host)
indata = "get {0} {1}\n".format(in_path, out_path)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self._send_password()
stdout, stderr = p.communicate(indata)
if p.returncode != 0:
raise AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
raise AnsibleError("failed to transfer file from {0}:\n{1}\n{2}".format(in_path, stdout, stderr))
def close(self):
''' not applicable since we're executing openssh binaries '''
pass
self._connected = False