mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Merge pull request #13654 from sivel/paramiko-proxy-command
Add ProxyCommand support to the paramiko connection plugin
This commit is contained in:
commit
3ac0143cf1
5 changed files with 77 additions and 12 deletions
|
@ -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…
Reference in a new issue