Merge pull request #13654 from sivel/paramiko-proxy-command

Add ProxyCommand support to the paramiko connection plugin
pull/10490/merge
Matt Martz 8 years ago
commit 3ac0143cf1

@ -768,6 +768,17 @@ instead. Setting it to False will improve performance and is recommended when h
record_host_keys=True record_host_keys=True
.. _paramiko_proxy_command
proxy_command
=============
.. versionadded:: 2.1
Use an OpenSSH like ProxyCommand for proxying all Paramiko SSH connections through a bastion or jump host. Requires a minimum of Paramiko version 1.9.0. On Enterprise Linux 6 this is provided by ``python-paramiko1.10`` in the EPEL repository::
proxy_command = ssh -W "%h:%p" bastion
.. _openssh_settings: .. _openssh_settings:
OpenSSH Specific Settings OpenSSH Specific Settings

@ -246,6 +246,7 @@ ANSIBLE_SSH_CONTROL_PATH = get_config(p, 'ssh_connection', 'control_path',
ANSIBLE_SSH_PIPELINING = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, boolean=True) ANSIBLE_SSH_PIPELINING = get_config(p, 'ssh_connection', 'pipelining', 'ANSIBLE_SSH_PIPELINING', False, boolean=True)
ANSIBLE_SSH_RETRIES = get_config(p, 'ssh_connection', 'retries', 'ANSIBLE_SSH_RETRIES', 0, integer=True) ANSIBLE_SSH_RETRIES = get_config(p, 'ssh_connection', 'retries', 'ANSIBLE_SSH_RETRIES', 0, integer=True)
PARAMIKO_RECORD_HOST_KEYS = get_config(p, 'paramiko_connection', 'record_host_keys', 'ANSIBLE_PARAMIKO_RECORD_HOST_KEYS', True, boolean=True) PARAMIKO_RECORD_HOST_KEYS = get_config(p, 'paramiko_connection', 'record_host_keys', 'ANSIBLE_PARAMIKO_RECORD_HOST_KEYS', True, boolean=True)
PARAMIKO_PROXY_COMMAND = get_config(p, 'paramiko_connection', 'proxy_command', 'ANSIBLE_PARAMIKO_PROXY_COMMAND', None)
# obsolete -- will be formally removed # obsolete -- will be formally removed

@ -23,6 +23,7 @@ __metaclass__ = type
import fcntl import fcntl
import gettext import gettext
import os import os
import shlex
from abc import ABCMeta, abstractmethod, abstractproperty from abc import ABCMeta, abstractmethod, abstractproperty
from functools import wraps from functools import wraps
@ -31,6 +32,7 @@ from ansible.compat.six import with_metaclass
from ansible import constants as C from ansible import constants as C
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.plugins import shell_loader from ansible.plugins import shell_loader
from ansible.utils.unicode import to_bytes, to_unicode
try: try:
from __main__ import display from __main__ import display
@ -119,6 +121,15 @@ class ConnectionBase(with_metaclass(ABCMeta, object)):
''' '''
pass pass
@staticmethod
def _split_ssh_args(argstring):
"""
Takes a string like '-o Foo=1 -o Bar="foo bar"' and returns a
list ['-o', 'Foo=1', '-o', 'Bar=foo bar'] that can be added to
the argument list. The list will not contain any empty elements.
"""
return [to_unicode(x.strip()) for x in shlex.split(to_bytes(argstring)) if x.strip()]
@abstractproperty @abstractproperty
def transport(self): def transport(self):
"""String used to identify this Connection class from other classes""" """String used to identify this Connection class from other classes"""

@ -32,6 +32,7 @@ import tempfile
import traceback import traceback
import fcntl import fcntl
import sys import sys
import re
from termios import tcflush, TCIFLUSH from termios import tcflush, TCIFLUSH
from binascii import hexlify from binascii import hexlify
@ -55,6 +56,9 @@ The %s key fingerprint is %s.
Are you sure you want to continue connecting (yes/no)? Are you sure you want to continue connecting (yes/no)?
""" """
# SSH Options Regex
SETTINGS_REGEX = re.compile(r'(\w+)(?:\s*=\s*|\s+)(.+)')
# prevent paramiko warning noise -- see http://stackoverflow.com/questions/3920502/ # prevent paramiko warning noise -- see http://stackoverflow.com/questions/3920502/
HAVE_PARAMIKO=False HAVE_PARAMIKO=False
with warnings.catch_warnings(): with warnings.catch_warnings():
@ -137,6 +141,51 @@ class Connection(ConnectionBase):
self.ssh = SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached() self.ssh = SSH_CONNECTION_CACHE[cache_key] = self._connect_uncached()
return self return self
def _parse_proxy_command(self, port=22):
proxy_command = None
# Parse ansible_ssh_common_args, specifically looking for ProxyCommand
ssh_args = [
getattr(self._play_context, 'ssh_extra_args', ''),
getattr(self._play_context, 'ssh_common_args', ''),
getattr(self._play_context, 'ssh_args', ''),
]
if ssh_common_args is not None:
args = self._split_ssh_args(' '.join(ssh_args))
for i, arg in enumerate(args):
if arg.lower() == 'proxycommand':
# _split_ssh_args split ProxyCommand from the command itself
proxy_command = args[i + 1]
else:
# ProxyCommand and the command itself are a single string
match = SETTINGS_REGEX.match(arg)
if match:
if match.group(1).lower() == 'proxycommand':
proxy_command = match.group(2)
if proxy_command:
break
proxy_command = proxy_command or C.PARAMIKO_PROXY_COMMAND
sock_kwarg = {}
if proxy_command:
replacers = {
'%h': self._play_context.remote_addr,
'%p': port,
'%r': self._play_context.remote_user
}
for find, replace in replacers.items():
proxy_command = proxy_command.replace(find, str(replace))
try:
sock_kwarg = {'sock': paramiko.ProxyCommand(proxy_command)}
display.vvv("CONFIGURE PROXY COMMAND FOR CONNECTION: %s" % proxy_command, host=self._play_context.remote_addr)
except AttributeError:
display.warning('Paramiko ProxyCommand support unavailable. '
'Please upgrade to Paramiko 1.9.0 or newer. '
'Not using configured ProxyCommand')
return sock_kwarg
def _connect_uncached(self): def _connect_uncached(self):
''' activates the connection object ''' ''' activates the connection object '''
@ -160,6 +209,8 @@ class Connection(ConnectionBase):
pass # file was not found, but not required to function pass # file was not found, but not required to function
ssh.load_system_host_keys() ssh.load_system_host_keys()
sock_kwarg = self._parse_proxy_command(port)
ssh.set_missing_host_key_policy(MyAddPolicy(self._new_stdin, self)) ssh.set_missing_host_key_policy(MyAddPolicy(self._new_stdin, self))
allow_agent = True allow_agent = True
@ -181,6 +232,7 @@ class Connection(ConnectionBase):
password=self._play_context.password, password=self._play_context.password,
timeout=self._play_context.timeout, timeout=self._play_context.timeout,
port=port, port=port,
**sock_kwarg
) )
except Exception as e: except Exception as e:
msg = str(e) msg = str(e)

@ -24,7 +24,6 @@ import os
import pipes import pipes
import pty import pty
import select import select
import shlex
import subprocess import subprocess
import time import time
@ -101,15 +100,6 @@ class Connection(ConnectionBase):
return controlpersist, controlpath return controlpersist, controlpath
@staticmethod
def _split_args(argstring):
"""
Takes a string like '-o Foo=1 -o Bar="foo bar"' and returns a
list ['-o', 'Foo=1', '-o', 'Bar=foo bar'] that can be added to
the argument list. The list will not contain any empty elements.
"""
return [to_unicode(x.strip()) for x in shlex.split(to_bytes(argstring)) if x.strip()]
def _add_args(self, explanation, args): def _add_args(self, explanation, args):
""" """
Adds the given args to self._command and displays a caller-supplied Adds the given args to self._command and displays a caller-supplied
@ -158,7 +148,7 @@ class Connection(ConnectionBase):
# Next, we add [ssh_connection]ssh_args from ansible.cfg. # Next, we add [ssh_connection]ssh_args from ansible.cfg.
if self._play_context.ssh_args: if self._play_context.ssh_args:
args = self._split_args(self._play_context.ssh_args) args = self._split_ssh_args(self._play_context.ssh_args)
self._add_args("ansible.cfg set ssh_args", args) self._add_args("ansible.cfg set ssh_args", args)
# Now we add various arguments controlled by configuration file settings # Now we add various arguments controlled by configuration file settings
@ -211,7 +201,7 @@ class Connection(ConnectionBase):
for opt in ['ssh_common_args', binary + '_extra_args']: for opt in ['ssh_common_args', binary + '_extra_args']:
attr = getattr(self._play_context, opt, None) attr = getattr(self._play_context, opt, None)
if attr is not None: if attr is not None:
args = self._split_args(attr) args = self._split_ssh_args(attr)
self._add_args("PlayContext set %s" % opt, args) self._add_args("PlayContext set %s" % opt, args)
# Check if ControlPersist is enabled and add a ControlPath if one hasn't # Check if ControlPersist is enabled and add a ControlPath if one hasn't

Loading…
Cancel
Save