Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow configuring sftp/scp executables #36648

Merged
merged 4 commits into from Apr 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 20 additions & 4 deletions lib/ansible/plugins/connection/ssh.py
Expand Up @@ -65,9 +65,24 @@
env: [{name: ANSIBLE_SSH_EXECUTABLE}]
ini:
- {key: ssh_executable, section: ssh_connection}
yaml: {key: ssh_connection.ssh_executable}
#const: ANSIBLE_SSH_EXECUTABLE
version_added: "2.2"
sftp_executable:
default: sftp
description:
- This defines the location of the sftp binary. It defaults to `sftp` which will use the first binary available in $PATH.
env: [{name: ANSIBLE_SFTP_EXECUTABLE}]
ini:
- {key: sftp_executable, section: ssh_connection}
version_added: "2.6"
scp_executable:
default: scp
description:
- This defines the location of the scp binary. It defaults to `scp` which will use the first binary available in $PATH.
env: [{name: ANSIBLE_SCP_EXECUTABLE}]
ini:
- {key: scp_executable, section: ssh_connection}
version_added: "2.6"
scp_extra_args:
description: Extra exclusive to the 'scp' CLI
vars:
Expand Down Expand Up @@ -913,15 +928,16 @@ def _file_transport_command(self, in_path, out_path, sftp_action):
for method in methods:
returncode = stdout = stderr = None
if method == 'sftp':
cmd = self._build_command('sftp', to_bytes(host))
cmd = self._build_command(self.get_option('sftp_executable'), to_bytes(host))
in_data = u"{0} {1} {2}\n".format(sftp_action, shlex_quote(in_path), shlex_quote(out_path))
in_data = to_bytes(in_data, nonstring='passthru')
(returncode, stdout, stderr) = self._bare_run(cmd, in_data, checkrc=False)
elif method == 'scp':
scp = self.get_option('scp_executable')
if sftp_action == 'get':
cmd = self._build_command('scp', u'{0}:{1}'.format(host, shlex_quote(in_path)), out_path)
cmd = self._build_command(scp, u'{0}:{1}'.format(host, shlex_quote(in_path)), out_path)
else:
cmd = self._build_command('scp', in_path, u'{0}:{1}'.format(host, shlex_quote(out_path)))
cmd = self._build_command(scp, in_path, u'{0}:{1}'.format(host, shlex_quote(out_path)))
in_data = None
(returncode, stdout, stderr) = self._bare_run(cmd, in_data, checkrc=False)
elif method == 'piped':
Expand Down
37 changes: 10 additions & 27 deletions test/units/plugins/connection/test_ssh.py
Expand Up @@ -33,6 +33,7 @@
from ansible.module_utils._text import to_bytes
from ansible.playbook.play_context import PlayContext
from ansible.plugins.connection import ssh
from ansible.plugins.loader import connection_loader


class TestConnectionBaseClass(unittest.TestCase):
Expand Down Expand Up @@ -68,13 +69,13 @@ def test_plugins_connection_ssh_basic(self):
def test_plugins_connection_ssh__build_command(self):
pc = PlayContext()
new_stdin = StringIO()
conn = ssh.Connection(pc, new_stdin)
conn = connection_loader.get('ssh', pc, new_stdin)
conn._build_command('ssh')

def test_plugins_connection_ssh_exec_command(self):
pc = PlayContext()
new_stdin = StringIO()
conn = ssh.Connection(pc, new_stdin)
conn = connection_loader.get('ssh', pc, new_stdin)

conn._build_command = MagicMock()
conn._build_command.return_value = 'ssh something something'
Expand All @@ -90,7 +91,7 @@ def test_plugins_connection_ssh__examine_output(self):
pc = PlayContext()
new_stdin = StringIO()

conn = ssh.Connection(pc, new_stdin)
conn = connection_loader.get('ssh', pc, new_stdin)

conn.check_password_prompt = MagicMock()
conn.check_become_success = MagicMock()
Expand Down Expand Up @@ -198,7 +199,7 @@ def _check_missing_password(line):
def test_plugins_connection_ssh_put_file(self, mock_ospe, mock_sleep):
pc = PlayContext()
new_stdin = StringIO()
conn = ssh.Connection(pc, new_stdin)
conn = connection_loader.get('ssh', pc, new_stdin)
conn._build_command = MagicMock()
conn._bare_run = MagicMock()

Expand Down Expand Up @@ -255,9 +256,10 @@ def test_plugins_connection_ssh_put_file(self, mock_ospe, mock_sleep):
def test_plugins_connection_ssh_fetch_file(self, mock_sleep):
pc = PlayContext()
new_stdin = StringIO()
conn = ssh.Connection(pc, new_stdin)
conn = connection_loader.get('ssh', pc, new_stdin)
conn._build_command = MagicMock()
conn._bare_run = MagicMock()
conn._load_name = 'ssh'

conn._build_command.return_value = 'some command to run'
conn._bare_run.return_value = (0, '', '')
Expand All @@ -269,6 +271,7 @@ def test_plugins_connection_ssh_fetch_file(self, mock_sleep):
# Test when SFTP works
C.DEFAULT_SCP_IF_SSH = 'smart'
expected_in_data = b' '.join((b'get', to_bytes(shlex_quote('/path/to/in/file')), to_bytes(shlex_quote('/path/to/dest/file')))) + b'\n'
conn.set_options({})
conn.fetch_file('/path/to/in/file', '/path/to/dest/file')
conn._bare_run.assert_called_with('some command to run', expected_in_data, checkrc=False)

Expand Down Expand Up @@ -327,10 +330,11 @@ def mock_run_env(request, mocker):
pc = PlayContext()
new_stdin = StringIO()

conn = ssh.Connection(pc, new_stdin)
conn = connection_loader.get('ssh', pc, new_stdin)
conn._send_initial_data = MagicMock()
conn._examine_output = MagicMock()
conn._terminate_process = MagicMock()
conn._load_name = 'ssh'
conn.sshpass_pipe = [MagicMock(), MagicMock()]

request.cls.pc = pc
Expand Down Expand Up @@ -469,27 +473,6 @@ def test_password_with_become(self):
assert self.conn._send_initial_data.call_count == 1
assert self.conn._send_initial_data.call_args[0][1] == 'this is input data'

def test_pasword_without_data(self):
# simulate no data input
self.mock_openpty.return_value = (98, 99)
self.mock_popen_res.stdout.read.side_effect = [b"some data", b"", b""]
self.mock_popen_res.stderr.read.side_effect = [b""]
self.mock_selector.select.side_effect = [
[(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
[(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
[(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
[(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
[]]
self.mock_selector.get_map.side_effect = lambda: True

return_code, b_stdout, b_stderr = self.conn._run("ssh", "")
assert return_code == 0
assert b_stdout == b'some data'
assert b_stderr == b''
assert self.mock_selector.register.called is True
assert self.mock_selector.register.call_count == 2
assert self.conn._send_initial_data.called is False

def test_pasword_without_data(self):
# simulate no data input but Popen using new pty's fails
self.mock_popen.return_value = None
Expand Down