From c56acfab943e6f3522dffd621c8a0a0b4f002fd9 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Sat, 15 Apr 2017 07:12:58 -0700 Subject: [PATCH 1/4] Add WinSSH communicator --- plugins/communicators/ssh/communicator.rb | 16 +- plugins/communicators/winssh/communicator.rb | 157 ++++++ plugins/communicators/winssh/config.rb | 25 + plugins/communicators/winssh/plugin.rb | 21 + plugins/provisioners/shell/provisioner.rb | 58 +- .../communicators/winssh/communicator_test.rb | 525 ++++++++++++++++++ 6 files changed, 795 insertions(+), 7 deletions(-) create mode 100644 plugins/communicators/winssh/communicator.rb create mode 100644 plugins/communicators/winssh/config.rb create mode 100644 plugins/communicators/winssh/plugin.rb create mode 100644 test/unit/plugins/communicators/winssh/communicator_test.rb diff --git a/plugins/communicators/ssh/communicator.rb b/plugins/communicators/ssh/communicator.rb index 8e71ad02f..2b0c2db7b 100644 --- a/plugins/communicators/ssh/communicator.rb +++ b/plugins/communicators/ssh/communicator.rb @@ -143,7 +143,7 @@ module VagrantPlugins # If we're already attempting to switch out the SSH key, then # just return that we're ready (for Machine#guest). @lock.synchronize do - return true if @inserted_key || !@machine.config.ssh.insert_key + return true if @inserted_key || !machine_config_ssh.insert_key @inserted_key = true end @@ -458,9 +458,9 @@ module VagrantPlugins # Determine the shell to execute. Prefer the explicitly passed in shell # over the default configured shell. If we are using `sudo` then we # need to wrap the shell in a `sudo` call. - cmd = @machine.config.ssh.shell + cmd = machine_config_ssh.shell cmd = shell if shell - cmd = @machine.config.ssh.sudo_command.gsub("%c", cmd) if sudo + cmd = machine_config_ssh.sudo_command.gsub("%c", cmd) if sudo cmd end @@ -482,7 +482,7 @@ module VagrantPlugins # Open the channel so we can execute or command channel = connection.open_channel do |ch| - if @machine.config.ssh.pty + if machine_config_ssh.pty ch.request_pty do |ch2, success| pty = success && command != "" @@ -611,7 +611,7 @@ module VagrantPlugins begin keep_alive = nil - if @machine.config.ssh.keep_alive + if machine_config_ssh.keep_alive # Begin sending keep-alive packets while we wait for the script # to complete. This avoids connections closing on long-running # scripts. @@ -687,9 +687,13 @@ module VagrantPlugins end def generate_environment_export(env_key, env_value) - template = @machine.config.ssh.export_command_template + template = machine_config_ssh.export_command_template template.sub("%ENV_KEY%", env_key).sub("%ENV_VALUE%", env_value) + "\n" end + + def machine_config_ssh + @machine.config.ssh + end end end end diff --git a/plugins/communicators/winssh/communicator.rb b/plugins/communicators/winssh/communicator.rb new file mode 100644 index 000000000..8dbe04d7d --- /dev/null +++ b/plugins/communicators/winssh/communicator.rb @@ -0,0 +1,157 @@ +require File.expand_path("../../ssh/communicator", __FILE__) + +module VagrantPlugins + module CommunicatorWinSSH + # This class provides communication with a Windows VM running + # the Windows native port of OpenSSH + class Communicator < VagrantPlugins::CommunicatorSSH::Communicator + + def initialize(machine) + super + @logger = Log4r::Logger.new("vagrant::communication::winssh") + end + + # Executes the command on an SSH connection within a login shell. + def shell_execute(connection, command, **opts) + opts = { + sudo: false, + shell: nil + }.merge(opts) + + sudo = opts[:sudo] + + @logger.info("Execute: #{command}") + exit_status = nil + + # Open the channel so we can execute or command + channel = connection.open_channel do |ch| + marker_found = false + data_buffer = '' + stderr_marker_found = false + stderr_data_buffer = '' + + tfile = Tempfile.new('vagrant-ssh') + remote_ext = machine_config_ssh.shell.to_s == "powershell" ? "ps1" : "bat" + remote_name = "C:\\Windows\\Temp\\#{File.basename(tfile.path)}.#{remote_ext}" + + if machine_config_ssh.shell.to_s == "powershell" + base_cmd = "powershell -File #{remote_name}" + tfile.puts <<-SCRIPT.force_encoding('ASCII-8BIT') +Remove-Item #{remote_name} +Write-Host #{CMD_GARBAGE_MARKER} +[Console]::Error.WriteLine("#{CMD_GARBAGE_MARKER}") +#{command} +SCRIPT + else + base_cmd = remote_name + tfile.puts <<-SCRIPT.force_encoding('ASCII-8BIT') +ECHO OFF +ECHO #{CMD_GARBAGE_MARKER} +ECHO #{CMD_GARBAGE_MARKER} 1>&2 +#{command} +SCRIPT + end + + tfile.close + upload(tfile.path, remote_name) + tfile.delete + + ch.exec(base_cmd) do |ch2, _| + # Setup the channel callbacks so we can get data and exit status + ch2.on_data do |ch3, data| + # Filter out the clear screen command + data = remove_ansi_escape_codes(data) + + if !marker_found + data_buffer << data + marker_index = data_buffer.index(CMD_GARBAGE_MARKER) + if marker_index + marker_found = true + data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size) + data.replace(data_buffer) + data_buffer = nil + end + end + + if block_given? && marker_found + yield :stdout, data + end + end + + ch2.on_extended_data do |ch3, type, data| + # Filter out the clear screen command + data = remove_ansi_escape_codes(data) + @logger.debug("stderr: #{data}") + if !stderr_marker_found + stderr_data_buffer << data + marker_index = stderr_data_buffer.index(CMD_GARBAGE_MARKER) + if marker_index + marker_found = true + stderr_data_buffer.slice!(0, marker_index + CMD_GARBAGE_MARKER.size) + data.replace(stderr_data_buffer.lstrip) + data_buffer = nil + end + end + + if block_given? && marker_found + yield :stderr, data + end + end + + ch2.on_request("exit-status") do |ch3, data| + exit_status = data.read_long + @logger.debug("Exit status: #{exit_status}") + + # Close the channel, since after the exit status we're + # probably done. This fixes up issues with hanging. + ch.close + end + + end + end + + begin + keep_alive = nil + + if @machine.config.ssh.keep_alive + # Begin sending keep-alive packets while we wait for the script + # to complete. This avoids connections closing on long-running + # scripts. + keep_alive = Thread.new do + loop do + sleep 5 + @logger.debug("Sending SSH keep-alive...") + connection.send_global_request("keep-alive@openssh.com") + end + end + end + + # Wait for the channel to complete + begin + channel.wait + rescue Errno::ECONNRESET, IOError + @logger.info( + "SSH connection unexpected closed. Assuming reboot or something.") + exit_status = 0 + pty = false + rescue Net::SSH::ChannelOpenFailed + raise Vagrant::Errors::SSHChannelOpenFail + rescue Net::SSH::Disconnect + raise Vagrant::Errors::SSHDisconnected + end + ensure + # Kill the keep-alive thread + keep_alive.kill if keep_alive + end + + # Return the final exit status + return exit_status + end + + def machine_config_ssh + @machine.config.winssh + end + + end + end +end diff --git a/plugins/communicators/winssh/config.rb b/plugins/communicators/winssh/config.rb new file mode 100644 index 000000000..cb7ee29c6 --- /dev/null +++ b/plugins/communicators/winssh/config.rb @@ -0,0 +1,25 @@ +require File.expand_path("../../../kernel_v2/config/ssh", __FILE__) + +module VagrantPlugins + module CommunicatorWinSSH + class Config < VagrantPlugins::Kernel_V2::SSHConfig + + def finalize! + @shell = "cmd" if @shell == UNSET_VALUE + @sudo_command = "cmd" if @sudo_command == UNSET_VALUE + if @export_command_template == UNSET_VALUE + if @shell == "cmd" + @export_command_template = 'set %ENV_KEY%="%ENV_VALUE%"' + else + @export_command_template = '$env:%ENV_KEY%="%ENV_VALUE%"' + end + end + super + end + + def to_s + "WINSSH" + end + end + end +end diff --git a/plugins/communicators/winssh/plugin.rb b/plugins/communicators/winssh/plugin.rb new file mode 100644 index 000000000..6fd4d023d --- /dev/null +++ b/plugins/communicators/winssh/plugin.rb @@ -0,0 +1,21 @@ +require "vagrant" + +module VagrantPlugins + module CommunicatorWinSSH + class Plugin < Vagrant.plugin("2") + name "windows ssh communicator" + description <<-DESC + DESC + + communicator("winssh") do + require File.expand_path("../communicator", __FILE__) + Communicator + end + + config("winssh") do + require_relative "config" + Config + end + end + end +end diff --git a/plugins/provisioners/shell/provisioner.rb b/plugins/provisioners/shell/provisioner.rb index 7c06d19df..8b01a5544 100644 --- a/plugins/provisioners/shell/provisioner.rb +++ b/plugins/provisioners/shell/provisioner.rb @@ -18,8 +18,11 @@ module VagrantPlugins args = " #{args.join(" ")}" end - if @machine.config.vm.communicator == :winrm + case @machine.config.vm.communicator + when :winrm provision_winrm(args) + when :winssh + provision_winssh(args) else provision_ssh(args) end @@ -94,6 +97,59 @@ module VagrantPlugins end end + # This is the provision method called if Windows OpenSSH is what is running + # on the remote end, which assumes a non-POSIX-style host. + def provision_winssh(args) + with_script_file do |path| + # Upload the script to the machine + @machine.communicate.tap do |comm| + env = config.env.map{|k,v| comm.generate_environment_export(k, v)}.join + remote_ext = @machine.config.winssh.shell == "powershell" ? "ps1" : "bat" + upload_path = "C:\\Windows\\Temp\\#{File.basename(path)}.#{remote_ext}" + if remote_ext == "ps1" + # Copy powershell_args from configuration + shell_args = config.powershell_args + # For PowerShell scripts bypass the execution policy unless already specified + shell_args += " -ExecutionPolicy Bypass" if config.powershell_args !~ /[-\/]ExecutionPolicy/i + # CLIXML output is kinda useless, especially on non-windows hosts + shell_args += " -OutputFormat Text" if config.powershell_args !~ /[-\/]OutputFormat/i + command = "#{env}\npowershell #{shell_args} #{upload_path}#{args}" + else + command = "#{env}\n#{upload_path}#{args}" + end + + # Reset upload path permissions for the current ssh user + info = nil + retryable(on: Vagrant::Errors::SSHNotReady, tries: 3, sleep: 2) do + info = @machine.ssh_info + raise Vagrant::Errors::SSHNotReady if info.nil? + end + + comm.upload(path.to_s, upload_path) + + if config.name + @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", + script: "script: #{config.name}")) + elsif config.path + @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", + script: path.to_s)) + else + @machine.ui.detail(I18n.t("vagrant.provisioners.shell.running", + script: "inline script")) + end + + # Execute it with sudo + comm.execute( + command, + sudo: config.privileged, + error_key: :ssh_bad_exit_status_muted + ) do |type, data| + handle_comm(type, data) + end + end + end + end + # This provisions using WinRM, which assumes a PowerShell # console on the other side. def provision_winrm(args) diff --git a/test/unit/plugins/communicators/winssh/communicator_test.rb b/test/unit/plugins/communicators/winssh/communicator_test.rb new file mode 100644 index 000000000..459cac6d4 --- /dev/null +++ b/test/unit/plugins/communicators/winssh/communicator_test.rb @@ -0,0 +1,525 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/communicators/winssh/communicator") + +describe VagrantPlugins::CommunicatorWinSSH::Communicator do + include_context "unit" + + let(:export_command_template){ 'export %ENV_KEY%="%ENV_VALUE%"' } + + let(:ssh) do + double("ssh", + timeout: 1, + host: nil, + port: 5986, + guest_port: 5986, + keep_alive: false + ) + end + + # SSH configuration information mock + let(:winssh) do + double("winssh", + insert_key: false, + export_command_template: export_command_template, + shell: 'cmd' + ) + end + # Configuration mock + let(:config) { double("config", winssh: winssh, ssh: ssh) } + # Provider mock + let(:provider) { double("provider") } + # UI mock + let(:ui) { double("ui") } + # Machine mock built with previously defined + let(:machine) do + double("machine", + config: config, + provider: provider, + ui: ui + ) + end + # Subject instance to test + let(:communicator){ @communicator ||= described_class.new(machine) } + # Underlying net-ssh connection mock + let(:connection) { double("connection") } + # Base net-ssh connection channel mock + let(:channel) { double("channel") } + # net-ssh connection channel mock for running commands + let(:command_channel) { double("command_channel") } + # Default exit data for commands run + let(:exit_data) { double("exit_data", read_long: 0) } + # Marker used for flagging start of output + let(:command_garbage_marker) { communicator.class.const_get(:CMD_GARBAGE_MARKER) } + # Start marker output when PTY is enabled + let(:pty_delim_start) { communicator.class.const_get(:PTY_DELIM_START) } + # End marker output when PTY is enabled + let(:pty_delim_end) { communicator.class.const_get(:PTY_DELIM_END) } + # Command output returned on stdout + let(:command_stdout_data) { '' } + # Command output returned on stderr + let(:command_stderr_data) { '' } + # Mock for net-ssh scp + let(:scp) { double("scp") } + # Stub file to match commands + let(:ssh_cmd_file){ double("ssh_cmd_file", path: "/dev/null/path") } + + # Setup for commands using the net-ssh connection. This can be reused where needed + # by providing to `before` + connection_setup = proc do + allow(connection).to receive(:logger) + allow(connection).to receive(:closed?).and_return false + allow(connection).to receive(:open_channel). + and_yield(channel).and_return(channel) + allow(channel).to receive(:wait).and_return true + allow(channel).to receive(:close) + allow(command_channel).to receive(:send_data) + allow(command_channel).to receive(:eof!) + allow(command_channel).to receive(:on_data). + and_yield(nil, command_stdout_data) + allow(command_channel).to receive(:on_extended_data). + and_yield(nil, nil, command_stderr_data) + allow(machine).to receive(:ssh_info).and_return(host: '10.1.2.3', port: 22) + allow(channel).to receive(:[]=).with(any_args).and_return(true) + allow(channel).to receive(:on_close) + allow(channel).to receive(:on_data) + allow(channel).to receive(:on_extended_data) + allow(channel).to receive(:on_request) + allow(channel).to receive(:on_process) + allow(channel).to receive(:exec).with(anything). + and_yield(command_channel, '').and_return channel + expect(command_channel).to receive(:on_request).with('exit-status'). + and_yield(nil, exit_data) + # Return mocked net-ssh connection during setup + allow(communicator).to receive(:retryable).and_return(connection) + allow(Tempfile).to receive(:new).with(/vagrant-ssh/).and_return(ssh_cmd_file) + allow(ssh_cmd_file).to receive(:puts) + allow(ssh_cmd_file).to receive(:close) + allow(ssh_cmd_file).to receive(:delete) + allow(scp).to receive(:upload!) + allow(communicator).to receive(:scp_connect).and_return(true) + end + + describe ".wait_for_ready" do + before(&connection_setup) + context "with no static config (default scenario)" do + before do + allow(ui).to receive(:detail) + end + + context "when ssh_info requires a multiple tries before it is ready" do + before do + expect(machine).to receive(:ssh_info). + and_return(nil).ordered + expect(machine).to receive(:ssh_info). + and_return(host: '10.1.2.3', port: 22).ordered + end + + it "retries ssh_info until ready" do + # retries are every 0.5 so buffer the timeout just a hair over + expect(communicator.wait_for_ready(0.6)).to eq(true) + end + end + end + end + + describe ".ready?" do + before(&connection_setup) + it "returns true if shell test is successful" do + expect(communicator.ready?).to be_true + end + + context "with an invalid shell test" do + before do + expect(exit_data).to receive(:read_long).and_return 1 + end + + it "returns raises SSHInvalidShell error" do + expect{ communicator.ready? }.to raise_error Vagrant::Errors::SSHInvalidShell + end + end + end + + describe ".execute" do + before(&connection_setup) + it "runs valid command and returns successful status code" do + expect(ssh_cmd_file).to receive(:puts).with(/dir/) + expect(communicator.execute("dir")).to eq(0) + end + + it "prepends UUID output to command for garbage removal" do + expect(ssh_cmd_file).to receive(:puts). + with(/ECHO OFF\nECHO #{command_garbage_marker}\nECHO #{command_garbage_marker}.*/) + expect(communicator.execute("dir")).to eq(0) + end + + context "with command returning an error" do + let(:exit_data) { double("exit_data", read_long: 1) } + + it "raises error when exit-code is non-zero" do + expect(ssh_cmd_file).to receive(:puts).with(/dir/) + expect{ communicator.execute("dir") }.to raise_error(Vagrant::Errors::VagrantError) + end + + it "returns exit-code when exit-code is non-zero and error check is disabled" do + expect(ssh_cmd_file).to receive(:puts).with(/dir/) + expect(communicator.execute("dir", error_check: false)).to eq(1) + end + end + + context "with garbage content prepended to command output" do + let(:command_stdout_data) do + "Line of garbage\nMore garbage\n#{command_garbage_marker}Dir1\nDir2\n" + end + + it "removes any garbage output prepended to command output" do + stdout = '' + expect( + communicator.execute("dir") do |type, data| + if type == :stdout + stdout << data + end + end + ).to eq(0) + expect(stdout).to eq("Dir1\nDir2\n") + end + end + + context "with garbage content prepended to command stderr output" do + let(:command_stderr_data) do + "Line of garbage\nMore garbage\n#{command_garbage_marker}Dir1\nDir2\n" + end + + it "removes any garbage output prepended to command stderr output" do + stderr = '' + expect( + communicator.execute("dir") do |type, data| + if type == :stderr + stderr << data + end + end + ).to eq(0) + expect(stderr).to eq("Dir1\nDir2\n") + end + end + end + + describe ".test" do + before(&connection_setup) + context "with exit code as zero" do + it "returns true" do + expect(communicator.test("dir")).to be_true + end + end + + context "with exit code as non-zero" do + before do + expect(exit_data).to receive(:read_long).and_return 1 + end + + it "returns false" do + expect(communicator.test("false.exe")).to be_false + end + end + end + + describe ".upload" do + before do + expect(communicator).to receive(:scp_connect).and_yield(scp) + end + + it "uploads a directory if local path is a directory" do + Dir.mktmpdir('vagrant-test') do |dir| + expect(scp).to receive(:upload!).with(dir, 'C:\destination', recursive: true) + communicator.upload(dir, 'C:\destination') + end + end + + it "uploads a file if local path is a file" do + file = Tempfile.new('vagrant-test') + begin + expect(scp).to receive(:upload!).with(instance_of(File), 'C:\destination\file') + communicator.upload(file.path, 'C:\destination\file') + ensure + file.delete + end + end + + it "raises custom error on permission errors" do + file = Tempfile.new('vagrant-test') + begin + expect(scp).to receive(:upload!).with(instance_of(File), 'C:\destination\file'). + and_raise("Permission denied") + expect{ communicator.upload(file.path, 'C:\destination\file') }.to( + raise_error(Vagrant::Errors::SCPPermissionDenied) + ) + ensure + file.delete + end + end + + it "does not raise custom error on non-permission errors" do + file = Tempfile.new('vagrant-test') + begin + expect(scp).to receive(:upload!).with(instance_of(File), 'C:\destination\file'). + and_raise("Some other error") + expect{ communicator.upload(file.path, 'C:\destination\file') }.to raise_error(RuntimeError) + ensure + file.delete + end + end + end + + describe ".download" do + before do + expect(communicator).to receive(:scp_connect).and_yield(scp) + end + + it "calls scp to download file" do + expect(scp).to receive(:download!).with('/path/from', 'C:\path\to') + communicator.download('/path/from', 'C:\path\to') + end + end + + describe ".connect" do + + it "cannot be called directly" do + expect{ communicator.connect }.to raise_error(NoMethodError) + end + + context "with default configuration" do + + before do + expect(machine).to receive(:ssh_info).and_return( + host: nil, + port: nil, + private_key_path: nil, + username: nil, + password: nil, + keys_only: true, + paranoid: false + ) + end + + it "has keys_only enabled" do + expect(Net::SSH).to receive(:start).with( + nil, nil, hash_including( + keys_only: true + ) + ).and_return(true) + communicator.send(:connect) + end + + it "has paranoid disabled" do + expect(Net::SSH).to receive(:start).with( + nil, nil, hash_including( + paranoid: false + ) + ).and_return(true) + communicator.send(:connect) + end + + it "does not include any private key paths" do + expect(Net::SSH).to receive(:start).with( + nil, nil, hash_excluding( + keys: anything + ) + ).and_return(true) + communicator.send(:connect) + end + + it "includes `none` and `hostbased` auth methods" do + expect(Net::SSH).to receive(:start).with( + nil, nil, hash_including( + auth_methods: ["none", "hostbased"] + ) + ).and_return(true) + communicator.send(:connect) + end + end + + context "with keys_only disabled and paranoid enabled" do + + before do + expect(machine).to receive(:ssh_info).and_return( + host: nil, + port: nil, + private_key_path: nil, + username: nil, + password: nil, + keys_only: false, + paranoid: true + ) + end + + it "has keys_only enabled" do + expect(Net::SSH).to receive(:start).with( + nil, nil, hash_including( + keys_only: false + ) + ).and_return(true) + communicator.send(:connect) + end + + it "has paranoid disabled" do + expect(Net::SSH).to receive(:start).with( + nil, nil, hash_including( + paranoid: true + ) + ).and_return(true) + communicator.send(:connect) + end + end + + context "with host and port configured" do + + before do + expect(machine).to receive(:ssh_info).and_return( + host: '127.0.0.1', + port: 2222, + private_key_path: nil, + username: nil, + password: nil, + keys_only: true, + paranoid: false + ) + end + + it "specifies configured host" do + expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, anything) + communicator.send(:connect) + end + + it "has port defined" do + expect(Net::SSH).to receive(:start).with("127.0.0.1", anything, hash_including(port: 2222)) + communicator.send(:connect) + end + end + + context "with private_key_path configured" do + before do + expect(machine).to receive(:ssh_info).and_return( + host: '127.0.0.1', + port: 2222, + private_key_path: ['/priv/key/path'], + username: nil, + password: nil, + keys_only: true, + paranoid: false + ) + end + + it "includes private key paths" do + expect(Net::SSH).to receive(:start).with( + anything, anything, hash_including( + keys: ["/priv/key/path"] + ) + ).and_return(true) + communicator.send(:connect) + end + + it "includes `publickey` auth method" do + expect(Net::SSH).to receive(:start).with( + anything, anything, hash_including( + auth_methods: ["none", "hostbased", "publickey"] + ) + ).and_return(true) + communicator.send(:connect) + end + end + + context "with username and password configured" do + + before do + expect(machine).to receive(:ssh_info).and_return( + host: '127.0.0.1', + port: 2222, + private_key_path: nil, + username: 'vagrant', + password: 'vagrant', + keys_only: true, + paranoid: false + ) + end + + it "has username defined" do + expect(Net::SSH).to receive(:start).with(anything, 'vagrant', anything).and_return(true) + communicator.send(:connect) + end + + it "has password defined" do + expect(Net::SSH).to receive(:start).with( + anything, anything, hash_including( + password: 'vagrant' + ) + ).and_return(true) + communicator.send(:connect) + end + + it "includes `password` auth method" do + expect(Net::SSH).to receive(:start).with( + anything, anything, hash_including( + auth_methods: ["none", "hostbased", "password"] + ) + ).and_return(true) + communicator.send(:connect) + end + end + + context "with password and private_key_path configured" do + + before do + expect(machine).to receive(:ssh_info).and_return( + host: '127.0.0.1', + port: 2222, + private_key_path: ['/priv/key/path'], + username: 'vagrant', + password: 'vagrant', + keys_only: true, + paranoid: false + ) + end + + it "has password defined" do + expect(Net::SSH).to receive(:start).with( + anything, anything, hash_including( + password: 'vagrant' + ) + ).and_return(true) + communicator.send(:connect) + end + + it "includes private key paths" do + expect(Net::SSH).to receive(:start).with( + anything, anything, hash_including( + keys: ["/priv/key/path"] + ) + ).and_return(true) + communicator.send(:connect) + end + + it "includes `publickey` and `password` auth methods" do + expect(Net::SSH).to receive(:start).with( + anything, anything, hash_including( + auth_methods: ["none", "hostbased", "publickey", "password"] + ) + ).and_return(true) + communicator.send(:connect) + end + end + end + + describe ".generate_environment_export" do + it "should generate bourne shell compatible export" do + communicator.send(:generate_environment_export, "TEST", "value").should eq("export TEST=\"value\"\n") + end + + context "with custom template defined" do + let(:export_command_template){ "setenv %ENV_KEY% %ENV_VALUE%" } + + it "should generate custom export based on template" do + communicator.send(:generate_environment_export, "TEST", "value").should eq("setenv TEST value\n") + end + end + end +end From b35c68eacc4a10459101b6f85db9cbc6a2e18efa Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Sun, 16 Apr 2017 07:59:29 -0700 Subject: [PATCH 2/4] Allow sudo wrapping but default to no-op --- plugins/communicators/winssh/communicator.rb | 5 ++++- plugins/communicators/winssh/config.rb | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/communicators/winssh/communicator.rb b/plugins/communicators/winssh/communicator.rb index 8dbe04d7d..e0ca8226b 100644 --- a/plugins/communicators/winssh/communicator.rb +++ b/plugins/communicators/winssh/communicator.rb @@ -20,7 +20,7 @@ module VagrantPlugins sudo = opts[:sudo] - @logger.info("Execute: #{command}") + @logger.info("Execute: #{command} (sudo=#{sudo.inspect})") exit_status = nil # Open the channel so we can execute or command @@ -56,6 +56,9 @@ SCRIPT upload(tfile.path, remote_name) tfile.delete + base_cmd = shell_cmd(opts.merge(shell: base_cmd)) + @logger.debug("Base SSH exec command: #{base_cmd}") + ch.exec(base_cmd) do |ch2, _| # Setup the channel callbacks so we can get data and exit status ch2.on_data do |ch3, data| diff --git a/plugins/communicators/winssh/config.rb b/plugins/communicators/winssh/config.rb index cb7ee29c6..10c6512a8 100644 --- a/plugins/communicators/winssh/config.rb +++ b/plugins/communicators/winssh/config.rb @@ -1,12 +1,14 @@ require File.expand_path("../../../kernel_v2/config/ssh", __FILE__) +# forward_x11 pty sudo_command + module VagrantPlugins module CommunicatorWinSSH class Config < VagrantPlugins::Kernel_V2::SSHConfig def finalize! @shell = "cmd" if @shell == UNSET_VALUE - @sudo_command = "cmd" if @sudo_command == UNSET_VALUE + @sudo_command = "%c" if @sudo_command == UNSET_VALUE if @export_command_template == UNSET_VALUE if @shell == "cmd" @export_command_template = 'set %ENV_KEY%="%ENV_VALUE%"' From c042fa8b248ec5eac416c91b1e33fbf12f09454a Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Sun, 16 Apr 2017 08:02:39 -0700 Subject: [PATCH 3/4] Add configuration documentation for winssh communicator --- .../docs/vagrantfile/winssh_settings.html.md | 163 ++++++++++++++++++ website/source/layouts/docs.erb | 1 + 2 files changed, 164 insertions(+) create mode 100644 website/source/docs/vagrantfile/winssh_settings.html.md diff --git a/website/source/docs/vagrantfile/winssh_settings.html.md b/website/source/docs/vagrantfile/winssh_settings.html.md new file mode 100644 index 000000000..4c0da9753 --- /dev/null +++ b/website/source/docs/vagrantfile/winssh_settings.html.md @@ -0,0 +1,163 @@ +--- +layout: "docs" +page_title: "config.winssh - Vagrantfile" +sidebar_current: "vagrantfile-winssh" +description: |- + The settings within "config.winssh" relate to configuring how Vagrant + will access your machine over Windows OpenSSH. As with most Vagrant settings, the + defaults are typically fine, but you can fine tune whatever you would like. +--- + +# WinSSH + +The WinSSH communicator is built specifically for the Windows native +port of OpenSSH. It does not rely on a POSIX-like environment which +removes the requirement of extra software installation (like cygwin) +for proper functionality. + +_NOTE: The Windows native port of OpenSSH is still considered +"pre-release" and is non-production ready._ + +For more information, see the [Win32-OpenSSH project page](https://github.com/PowerShell/Win32-OpenSSH/). + +# WinSSH Settings + +The WinSSH communicator uses the same connection configuration options +as the SSH communicator. These settings provide the information for the +communicator to establish a connection to the VM. + +**Config namespace: `config.ssh`** + +The settings within `config.ssh` relate to configuring how Vagrant +will access your machine over SSH. As with most Vagrant settings, the +defaults are typically fine, but you can fine tune whatever you would like. + +## Available Settings + +`config.ssh.username` - This sets the username that Vagrant will SSH +as by default. Providers are free to override this if they detect a more +appropriate user. By default this is "vagrant," since that is what most +public boxes are made as. + +
+ +`config.ssh.password` - This sets a password that Vagrant will use to +authenticate the SSH user. Note that Vagrant recommends you use key-based +authentication rather than a password (see `private_key_path`) below. If +you use a password, Vagrant will automatically insert a keypair if +`insert_key` is true. + +
+ +`config.ssh.host` - The hostname or IP to SSH into. By default this is +empty, because the provider usually figures this out for you. + +
+ +`config.ssh.port` - The port to SSH into. By default this is port 22. + +
+ +`config.ssh.guest_port` - The port on the guest that SSH is running on. This +is used by some providers to detect forwarded ports for SSH. For example, if +this is set to 22 (the default), and Vagrant detects a forwarded port to +port 22 on the guest from port 4567 on the host, Vagrant will attempt +to use port 4567 to talk to the guest if there is no other option. + +
+ +`config.ssh.private_key_path` - The path to the private key to use to +SSH into the guest machine. By default this is the insecure private key +that ships with Vagrant, since that is what public boxes use. If you make +your own custom box with a custom SSH key, this should point to that +private key. + +You can also specify multiple private keys by setting this to be an array. +This is useful, for example, if you use the default private key to bootstrap +the machine, but replace it with perhaps a more secure key later. + +
+ +`config.ssh.insert_key` - If `true`, Vagrant will automatically insert +a keypair to use for SSH, replacing Vagrant's default insecure key +inside the machine if detected. By default, this is true. + +This only has an effect if you do not already use private keys for +authentication or if you are relying on the default insecure key. +If you do not have to care about security in your project and want to +keep using the default insecure key, set this to `false`. + +
+ +`config.ssh.keys_only` - Only use Vagrant-provided SSH private keys (do not use +any keys stored in ssh-agent). The default value is `true`.` + +
+ +`config.ssh.paranoid` - Perform strict host-key verification. The default value +is `false`. + +# WinSSH Settings + +The configuration options below are specific to the WinSSH communicator. + +**Config namespace: `config.winssh`** + +## Available Settings + +`config.winssh.forward_agent` - If `true`, agent forwarding over SSH +connections is enabled. Defaults to false. + +
+ +`config.winssh.forward_env` - An array of host environment variables to forward to +the guest. If you are familiar with OpenSSH, this corresponds to the `SendEnv` +parameter. + +```ruby +config.winssh.forward_env = ["CUSTOM_VAR"] +``` + +
+ +`config.winssh.proxy_command` - A command-line command to execute that receives +the data to send to SSH on stdin. This can be used to proxy the SSH connection. +`%h` in the command is replaced with the host and `%p` is replaced with +the port. + +
+ +`config.winssh.keep_alive` If `true`, this setting SSH will send keep-alive packets +every 5 seconds by default to keep connections alive. + +
+ +`config.winssh.shell` - The shell to use when executing SSH commands from +Vagrant. By default this is `cmd`. Valid values are `"cmd"` or `"powershell"`. +Note that this has no effect on the shell you get when you run `vagrant ssh`. +This configuration option only affects the shell to use when executing commands +internally in Vagrant. + +
+ +`config.winssh.export_command_template` - The template used to generate +exported environment variables in the active session. This can be useful +when using a Bourne incompatible shell like C shell. The template supports +two variables which are replaced with the desired environment variable key and +environment variable value: `%ENV_KEY%` and `%ENV_VALUE%`. The default template +for a `cmd` configured shell is: + +```ruby +config.winssh.export_command_template = 'set %ENV_KEY%="%ENV_VALUE%"' +``` + +The default template for a `powershell` configured shell is: + +```ruby +config.winssh.export_command_template = '$env:%ENV_KEY%="%ENV_VALUE%"' +``` + +`config.winssh.sudo_command` - The command to use when executing a command +with `sudo`. This defaults to `%c` (assumes vagrant user is an administator +and needs no escalation). The `%c` will be replaced by the command that is +being executed. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 178856880..5239cc8d6 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -69,6 +69,7 @@ >config.vm >config.ssh >config.winrm + >config.winssh >config.vagrant From 2c9cd87a71d168d12e75f0494351f3b0fdf84bc1 Mon Sep 17 00:00:00 2001 From: Chris Roberts Date: Sun, 16 Apr 2017 08:08:22 -0700 Subject: [PATCH 4/4] Remove unused configuration options --- plugins/communicators/winssh/config.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/communicators/winssh/config.rb b/plugins/communicators/winssh/config.rb index 10c6512a8..4ad03d19f 100644 --- a/plugins/communicators/winssh/config.rb +++ b/plugins/communicators/winssh/config.rb @@ -1,7 +1,5 @@ require File.expand_path("../../../kernel_v2/config/ssh", __FILE__) -# forward_x11 pty sudo_command - module VagrantPlugins module CommunicatorWinSSH class Config < VagrantPlugins::Kernel_V2::SSHConfig @@ -22,6 +20,11 @@ module VagrantPlugins def to_s "WINSSH" end + + # Remove configuration options from regular SSH that are + # not used within this communicator + undef :forward_x11 + undef :pty end end end