Enable Windows users with SSH installed to use 'vagrant ssh'
This commit is contained in:
parent
1d1dfec940
commit
bd06bea3e5
|
@ -0,0 +1,132 @@
|
||||||
|
require 'log4r'
|
||||||
|
|
||||||
|
require 'vagrant/util/file_util'
|
||||||
|
require 'vagrant/util/file_mode'
|
||||||
|
require 'vagrant/util/platform'
|
||||||
|
require 'vagrant/util/safe_exec'
|
||||||
|
|
||||||
|
module Vagrant
|
||||||
|
# Manages SSH connection information as well as allows opening an
|
||||||
|
# SSH connection.
|
||||||
|
class SSH
|
||||||
|
include Util::SafeExec
|
||||||
|
|
||||||
|
def initialize(vm)
|
||||||
|
@vm = vm
|
||||||
|
@logger = Log4r::Logger.new("vagrant::ssh")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a hash of information necessary for accessing this
|
||||||
|
# virtual machine via SSH.
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def info
|
||||||
|
results = {
|
||||||
|
:host => @vm.config.ssh.host,
|
||||||
|
:port => @vm.config.ssh.port || @vm.driver.ssh_port(@vm.config.ssh.guest_port),
|
||||||
|
:username => @vm.config.ssh.username,
|
||||||
|
:forward_agent => @vm.config.ssh.forward_agent,
|
||||||
|
:forward_x11 => @vm.config.ssh.forward_x11
|
||||||
|
}
|
||||||
|
|
||||||
|
# This can happen if no port is set and for some reason Vagrant
|
||||||
|
# can't detect an SSH port.
|
||||||
|
raise Errors::SSHPortNotDetected if !results[:port]
|
||||||
|
|
||||||
|
# Determine the private key path, which is either set by the
|
||||||
|
# configuration or uses just the built-in insecure key.
|
||||||
|
pk_path = @vm.config.ssh.private_key_path || @vm.env.default_private_key_path
|
||||||
|
results[:private_key_path] = File.expand_path(pk_path, @vm.env.root_path)
|
||||||
|
|
||||||
|
# We need to check and fix the private key permissions
|
||||||
|
# to make sure that SSH gets a key with 0600 perms.
|
||||||
|
check_key_permissions(results[:private_key_path])
|
||||||
|
|
||||||
|
# Return the results
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
|
||||||
|
# Connects to the environment's virtual machine, replacing the ruby
|
||||||
|
# process with an SSH process.
|
||||||
|
#
|
||||||
|
# @param [Hash] opts Options hash
|
||||||
|
# @options opts [Boolean] :plain_mode If True, doesn't authenticate with
|
||||||
|
# the machine, only connects, allowing the user to connect.
|
||||||
|
def exec(opts={})
|
||||||
|
# Get the SSH information and cache it here
|
||||||
|
ssh_info = info
|
||||||
|
|
||||||
|
# Ensure the platform supports ssh. On Windows there are several programs which
|
||||||
|
# include ssh, notably git, mingw and cygwin, but make sure ssh is in the path!
|
||||||
|
if !Util::FileUtil.which("ssh")
|
||||||
|
if Util::Platform.windows?
|
||||||
|
raise Errors::SSHUnavailableWindows, :host => ssh_info[:host],
|
||||||
|
:port => ssh_info[:port],
|
||||||
|
:username => ssh_info[:username],
|
||||||
|
:key_path => ssh_info[:private_key_path]
|
||||||
|
end
|
||||||
|
raise Errors::SSHUnavailable
|
||||||
|
end
|
||||||
|
|
||||||
|
# If plain mode is enabled then we don't do any authentication (we don't
|
||||||
|
# set a user or an identity file)
|
||||||
|
plain_mode = opts[:plain_mode]
|
||||||
|
|
||||||
|
options = {}
|
||||||
|
options[:host] = ssh_info[:host]
|
||||||
|
options[:port] = ssh_info[:port]
|
||||||
|
options[:username] = ssh_info[:username]
|
||||||
|
options[:private_key_path] = ssh_info[:private_key_path]
|
||||||
|
|
||||||
|
# Command line options
|
||||||
|
command_options = ["-p", options[:port].to_s, "-o", "UserKnownHostsFile=/dev/null",
|
||||||
|
"-o", "StrictHostKeyChecking=no", "-o", "LogLevel=QUIET"]
|
||||||
|
|
||||||
|
# Solaris/OpenSolaris/Illumos uses SunSSH which doesn't support the IdentitiesOnly option
|
||||||
|
# (Also don't use it in plain mode, it'll skip user agents.)
|
||||||
|
command_options += ["-o", "IdentitiesOnly=yes"] if !(Util::Platform.solaris? || plain_mode)
|
||||||
|
|
||||||
|
command_options += ["-i", options[:private_key_path]] if !plain_mode
|
||||||
|
command_options += ["-o", "ForwardAgent=yes"] if ssh_info[:forward_agent]
|
||||||
|
|
||||||
|
# If there are extra options, then we append those
|
||||||
|
command_options.concat(opts[:extra_args]) if opts[:extra_args]
|
||||||
|
|
||||||
|
if ssh_info[:forward_x11]
|
||||||
|
# Both are required so that no warnings are shown regarding X11
|
||||||
|
command_options += ["-o", "ForwardX11=yes"]
|
||||||
|
command_options += ["-o", "ForwardX11Trusted=yes"]
|
||||||
|
end
|
||||||
|
|
||||||
|
host_string = options[:host]
|
||||||
|
host_string = "#{options[:username]}@#{host_string}" if !plain_mode
|
||||||
|
command_options << host_string
|
||||||
|
@logger.info("Invoking SSH: #{command_options.inspect}")
|
||||||
|
safe_exec("ssh", *command_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks the file permissions for a private key, resetting them
|
||||||
|
# if needed.
|
||||||
|
def check_key_permissions(key_path)
|
||||||
|
# Windows systems don't have this issue
|
||||||
|
return if Util::Platform.windows?
|
||||||
|
|
||||||
|
@logger.debug("Checking key permissions: #{key_path}")
|
||||||
|
stat = File.stat(key_path)
|
||||||
|
|
||||||
|
if stat.owned? && Util::FileMode.from_octal(stat.mode) != "600"
|
||||||
|
@logger.info("Attempting to correct key permissions to 0600")
|
||||||
|
File.chmod(0600, key_path)
|
||||||
|
|
||||||
|
stat = File.stat(key_path)
|
||||||
|
if Util::FileMode.from_octal(stat.mode) != "600"
|
||||||
|
raise Errors::SSHKeyBadPermissions, :key_path => key_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Errno::EPERM
|
||||||
|
# This shouldn't happen since we verified we own the file, but
|
||||||
|
# it is possible in theory, so we raise an error.
|
||||||
|
raise Errors::SSHKeyBadPermissions, :key_path => key_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
module Vagrant
|
||||||
|
module Util
|
||||||
|
class FileUtil
|
||||||
|
# Cross-platform way of finding an executable in the $PATH.
|
||||||
|
#
|
||||||
|
# which('ruby') #=> /usr/bin/ruby
|
||||||
|
# by http://stackoverflow.com/users/11687/mislav
|
||||||
|
#
|
||||||
|
# This code is adapted from the following post by mislav:
|
||||||
|
# http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
|
||||||
|
def self.which(cmd)
|
||||||
|
|
||||||
|
# If the PATHEXT variable is empty, we're on *nix and need to find the exact filename
|
||||||
|
exts = nil
|
||||||
|
if !Util::Platform.windows? || ENV['PATHEXT'].nil?
|
||||||
|
exts = ['']
|
||||||
|
# On Windows: if filename contains an extension, we must match that exact filename
|
||||||
|
elsif File.extname(cmd).length != 0
|
||||||
|
exts = ['']
|
||||||
|
# On Windows: otherwise try to match all possible executable file extensions (.EXE .COM .BAT etc.)
|
||||||
|
else
|
||||||
|
exts = ENV['PATHEXT'].split(';')
|
||||||
|
end
|
||||||
|
|
||||||
|
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
||||||
|
exts.each do |ext|
|
||||||
|
exe = "#{path}#{File::SEPARATOR}#{cmd}#{ext}"
|
||||||
|
return exe if File.executable? exe
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,6 @@
|
||||||
require "log4r"
|
require "log4r"
|
||||||
|
|
||||||
|
require "vagrant/util/file_util"
|
||||||
require "vagrant/util/file_mode"
|
require "vagrant/util/file_mode"
|
||||||
require "vagrant/util/platform"
|
require "vagrant/util/platform"
|
||||||
require "vagrant/util/safe_exec"
|
require "vagrant/util/safe_exec"
|
||||||
|
@ -52,20 +53,19 @@ module Vagrant
|
||||||
# @param [Hash] opts These are additional options that are supported
|
# @param [Hash] opts These are additional options that are supported
|
||||||
# by exec.
|
# by exec.
|
||||||
def self.exec(ssh_info, opts={})
|
def self.exec(ssh_info, opts={})
|
||||||
# If we're running Windows, raise an exception since we currently
|
# Ensure the platform supports ssh. On Windows there are several programs which
|
||||||
# still don't support exec-ing into SSH. In the future this should
|
# include ssh, notably git, mingw and cygwin, but make sure ssh is in the path!
|
||||||
# certainly be possible if we can detect we're in an environment that
|
if !FileUtil.which("ssh")
|
||||||
# supports it.
|
if Platform.windows?
|
||||||
if Platform.windows?
|
raise Errors::SSHUnavailableWindows,
|
||||||
raise Errors::SSHUnavailableWindows,
|
:host => ssh_info[:host],
|
||||||
:host => ssh_info[:host],
|
:port => ssh_info[:port],
|
||||||
:port => ssh_info[:port],
|
:username => ssh_info[:username],
|
||||||
:username => ssh_info[:username],
|
:key_path => ssh_info[:private_key_path]
|
||||||
:key_path => ssh_info[:private_key_path]
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Verify that we have SSH available on the system.
|
raise Errors::SSHUnavailable
|
||||||
raise Errors::SSHUnavailable if !Kernel.system("which ssh > /dev/null 2>&1")
|
end
|
||||||
|
|
||||||
# If plain mode is enabled then we don't do any authentication (we don't
|
# If plain mode is enabled then we don't do any authentication (we don't
|
||||||
# set a user or an identity file)
|
# set a user or an identity file)
|
||||||
|
|
|
@ -298,9 +298,10 @@ en:
|
||||||
guest port value, or specify an explicit SSH port with `config.ssh.port`.
|
guest port value, or specify an explicit SSH port with `config.ssh.port`.
|
||||||
ssh_unavailable: "`ssh` binary could not be found. Is an SSH client installed?"
|
ssh_unavailable: "`ssh` binary could not be found. Is an SSH client installed?"
|
||||||
ssh_unavailable_windows: |-
|
ssh_unavailable_windows: |-
|
||||||
`vagrant ssh` isn't available on the Windows platform. You are still able
|
`ssh` executable not found in any directories in the %PATH% variable. Is an
|
||||||
to SSH into the virtual machine if you get a Windows SSH client (such as
|
SSH client installed? Try installing Cygwin, MinGW or Git, all of which
|
||||||
PuTTY). The authentication information is shown below:
|
contain an SSH client. Or use the PuTTY SSH client with the following
|
||||||
|
authentication information shown below:
|
||||||
|
|
||||||
Host: %{host}
|
Host: %{host}
|
||||||
Port: %{port}
|
Port: %{port}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
require File.expand_path("../../../base", __FILE__)
|
||||||
|
|
||||||
|
require 'vagrant/util/file_util'
|
||||||
|
require 'vagrant/util/platform'
|
||||||
|
|
||||||
|
describe Vagrant::Util::FileUtil do
|
||||||
|
|
||||||
|
def tester (file_extension, test_extension, mode, &block)
|
||||||
|
# create file in temp directory
|
||||||
|
filename = '__vagrant_unit_test__'
|
||||||
|
dir = Dir.tmpdir
|
||||||
|
file = Pathname(dir) + (filename + file_extension)
|
||||||
|
file.open("w") { |f| f.write("#") }
|
||||||
|
file.chmod(mode)
|
||||||
|
|
||||||
|
# set the path to the directory where the file is located
|
||||||
|
savepath = ENV['PATH']
|
||||||
|
ENV['PATH'] = dir.to_s
|
||||||
|
block.call filename + test_extension
|
||||||
|
ENV['PATH'] = savepath
|
||||||
|
|
||||||
|
file.unlink
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return a path for an executable file" do
|
||||||
|
tester '.bat', '.bat', 0755 do |name|
|
||||||
|
Vagrant::Util::FileUtil.which(name).should_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if Vagrant::Util::Platform.windows?
|
||||||
|
it "should return a path for a Windows executable file" do
|
||||||
|
tester '.bat', '', 0755 do |name|
|
||||||
|
Vagrant::Util::FileUtil.which(name).should_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return nil for a non-executable file" do
|
||||||
|
tester '.txt', '.txt', 0644 do |name|
|
||||||
|
Vagrant::Util::FileUtil.which(name).should be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in New Issue