Merge pull request #3530 from mitchellh/linux-command-filters-for-winrm-communicator

communicator/winrm: command filters for winrm communicator
This commit is contained in:
Mitchell Hashimoto 2014-04-24 01:38:34 -07:00
commit f36dfb2961
14 changed files with 410 additions and 58 deletions

View File

@ -0,0 +1,48 @@
module VagrantPlugins
module CommunicatorWinRM
# Handles loading and applying all available WinRM command filters
class CommandFilter
@@cmd_filters = [
"cat",
"chmod",
"chown",
"grep",
"rm",
"test",
"uname",
"which"
]
# Filter the given Vagrant command to ensure compatibility with Windows
#
# @param [String] The Vagrant shell command
# @returns [String] Windows runnable command or empty string
def filter(command)
command_filters.each { |c| command = c.filter(command) if c.accept?(command) }
command
end
# All the available Linux command filters
#
# @returns [Array] All Linux command filter instances
def command_filters
@command_filters ||= create_command_filters()
end
private
def create_command_filters
[].tap do |filters|
@@cmd_filters.each do |cmd|
require_relative "command_filters/#{cmd}"
class_name = "VagrantPlugins::CommunicatorWinRM::CommandFilters::#{cmd.capitalize}"
filters << Module.const_get(class_name).new
end
end
end
end
end
end

View File

@ -0,0 +1,27 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Handles the special case of determining the guest OS using cat
class Cat
def filter(command)
# cat /etc/release | grep -i OmniOS
# cat /etc/redhat-release
# cat /etc/issue | grep 'Core Linux'
# cat /etc/release | grep -i SmartOS
''
end
def accept?(command)
# cat works in PowerShell, however we don't want to run Guest
# OS detection as this will fail on Windows because the lack of the
# grep command
command.start_with?('cat /etc/')
end
end
end
end
end

View File

@ -0,0 +1,21 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Converts a *nix 'chmod' command to a PowerShell equivalent
class Chmod
def filter(command)
# Not support on Windows, the communicator will skip this command
''
end
def accept?(command)
command.start_with?('chmod ')
end
end
end
end
end

View File

@ -0,0 +1,21 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Converts a *nix 'chown' command to a PowerShell equivalent
class Chown
def filter(command)
# Not support on Windows, the communicator will skip this command
''
end
def accept?(command)
command.start_with?('chown ')
end
end
end
end
end

View File

@ -0,0 +1,26 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Converts a *nix 'grep' command to a PowerShell equivalent
class Grep
def filter(command)
# grep 'Fedora release [12][67890]' /etc/redhat-release
# grep Funtoo /etc/gentoo-release
# grep Gentoo /etc/gentoo-release
# grep is often used to detect the guest type in Vagrant, so don't bother running
# to speed up OS detection
''
end
def accept?(command)
command.start_with?('grep ')
end
end
end
end
end

View File

@ -0,0 +1,28 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Converts a *nix 'rm' command to a PowerShell equivalent
class Rm
def filter(command)
# rm -Rf /some/dir
# rm /some/dir
cmd_parts = command.strip.split(/\s+/)
dir = cmd_parts[1]
if dir == '-Rf'
dir = cmd_parts[2]
return "rm '#{dir}' -recurse -force"
end
return "rm '#{dir}' -force"
end
def accept?(command)
command.start_with?('rm ')
end
end
end
end
end

View File

@ -0,0 +1,68 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Converts a *nix 'test' command to a PowerShell equivalent
class Test
def filter(command)
# test -d /tmp/dir
# test -f /tmp/afile
# test -L /somelink
# test -x /tmp/some.exe
cmd_parts = command.strip.split(/\s+/)
flag = cmd_parts[1]
path = cmd_parts[2]
if flag == '-d'
check_for_directory(path)
elsif flag == '-f' || flag == '-x'
check_for_file(path)
else
check_exists(path)
end
end
def accept?(command)
command.start_with?("test ")
end
private
def check_for_directory(path)
<<-EOH
$p = "#{path}"
if ((Test-Path $p) -and (get-item $p).PSIsContainer) {
exit 0
}
exit 1
EOH
end
def check_for_file(path)
<<-EOH
$p = "#{path}"
if ((Test-Path $p) -and (!(get-item $p).PSIsContainer)) {
exit 0
}
exit 1
EOH
end
def check_exists(path)
<<-EOH
$p = "#{path}"
if (Test-Path $p) {
exit 0
}
exit 1
EOH
end
end
end
end
end

View File

@ -0,0 +1,31 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Converts a *nix 'uname' command to a PowerShell equivalent
class Uname
def filter(command)
# uname -s | grep 'Darwin'
# uname -s | grep VMkernel
# uname -s | grep 'FreeBSD'
# uname -s | grep 'Linux'
# uname -s | grep NetBSD
# uname -s | grep 'OpenBSD'
# uname -sr | grep SunOS | grep -v 5.11
# uname -sr | grep 'SunOS 5.11'
# uname is used to detect the guest type in Vagrant, so don't bother running
# to speed up OS detection
''
end
def accept?(command)
command.start_with?('uname ')
end
end
end
end
end

View File

@ -0,0 +1,26 @@
module VagrantPlugins
module CommunicatorWinRM
module CommandFilters
# Converts a *nix 'which' command to a PowerShell equivalent
class Which
def filter(command)
executable = command.strip.split(/\s+/)[1]
return <<-EOH
$command = [Array](Get-Command #{executable} -errorAction SilentlyContinue)
if ($null -eq $command) { exit 1 }
write-host $command[0].Definition
exit 0
EOH
end
def accept?(command)
command.start_with?('which ')
end
end
end
end
end

View File

@ -4,6 +4,7 @@ require "log4r"
require_relative "helper"
require_relative "shell"
require_relative "command_filter"
module VagrantPlugins
module CommunicatorWinRM
@ -16,9 +17,10 @@ module VagrantPlugins
end
def initialize(machine)
@machine = machine
@logger = Log4r::Logger.new("vagrant::communication::winrm")
@shell = nil
@machine = machine
@shell = nil
@logger = Log4r::Logger.new("vagrant::communication::winrm")
@cmd_filter = CommandFilter.new()
@logger.info("Initializing WinRMCommunicator")
end
@ -50,6 +52,10 @@ module VagrantPlugins
end
def execute(command, opts={}, &block)
# If this is a *nix command with no Windows equivilant, don't run it
command = @cmd_filter.filter(command)
return 0 if command.empty?
opts = {
:error_check => true,
:error_class => Errors::ExecutionError,
@ -58,26 +64,15 @@ module VagrantPlugins
:shell => :powershell
}.merge(opts || {})
if opts[:shell] == :powershell
script = File.expand_path("../scripts/command_alias.ps1", __FILE__)
script = File.read(script)
command = script << "\r\n" << command << "\r\nexit $LASTEXITCODE"
end
output = shell.send(opts[:shell], command, &block)
return output if opts[:shell] == :wql
exitcode = output[:exitcode]
raise_execution_error(opts, exitcode) if opts[:error_check] && exitcode != 0
exitcode
execution_output(output, opts)
end
alias_method :sudo, :execute
def test(command, opts=nil)
@logger.info("Testing: #{command}")
# HACK: to speed up Vagrant 1.2 OS detection, skip checking for *nix OS
return false unless (command =~ /^uname|^cat \/etc|^cat \/proc|grep 'Fedora/).nil?
# If this is a *nix command with no Windows equivilant, assume failure
command = @cmd_filter.filter(command)
return false if command.empty?
opts = { :error_check => false }.merge(opts || {})
execute(command, opts) == 0
@ -111,10 +106,21 @@ module VagrantPlugins
)
end
def raise_execution_error(opts, exit_code)
# Handles the raw WinRM shell result and converts it to a
# standard Vagrant communicator result
def execution_output(output, opts)
if opts[:shell] == :wql
return output
elsif opts[:error_check] && output[:exitcode] != 0
raise_execution_error(output, opts)
end
output[:exitcode]
end
def raise_execution_error(output, opts)
# The error classes expect the translation key to be _key, but that makes for an ugly
# configuration parameter, so we set it here from `error_key`
msg = "Command execution failed with an exit code of #{exit_code}"
msg = "Command execution failed with an exit code of #{output[:exitcode]}"
error_opts = opts.merge(:_key => opts[:error_key], :message => msg)
raise opts[:error_class], error_opts
end

View File

@ -1,35 +0,0 @@
function which {
$command = [Array](Get-Command $args[0] -errorAction SilentlyContinue)
if($null -eq $command)
{
exit 1
}
write-host $command[0].Definition
exit 0
}
function test ([Switch] $d, [String] $path) {
if(Test-Path $path)
{
exit 0
}
exit 1
}
function chmod {
exit 0
}
function chown {
exit 0
}
function mkdir ([Switch] $p, [String] $path)
{
if(Test-Path $path)
{
exit 0
} else {
New-Item $path -Type Directory -Force | Out-Null
}
}

View File

@ -52,6 +52,9 @@ module VagrantPlugins
end
def powershell(command, &block)
# ensure an exit code
command << "\r\n"
command << "if ($LASTEXITCODE) { exit $LASTEXITCODE } else { exit 0 }"
execute_shell(command, :powershell, &block)
end

View File

@ -0,0 +1,82 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/communicators/winrm/command_filter")
describe VagrantPlugins::CommunicatorWinRM::CommandFilter, :unit => true do
describe '.command_filters' do
it 'initializes all command filters in command filters directory' do
expect(subject.command_filters()).not_to be_empty
end
end
describe '.filter' do
it 'filters out uname commands' do
expect(subject.filter('uname -s stuff')).to eq('')
end
it 'filters out grep commands' do
expect(subject.filter("grep 'Fedora release [12][67890]' /etc/redhat-release")).to eq("")
end
it 'filters out which commands' do
expect(subject.filter('which ruby')).to include(
'[Array](Get-Command ruby -errorAction SilentlyContinue)')
end
it 'filters out test -d commands' do
expect(subject.filter('test -d /tmp/dir')).to include(
"$p = \"/tmp/dir\"")
expect(subject.filter('test -d /tmp/dir')).to include(
"if ((Test-Path $p) -and (get-item $p).PSIsContainer) {")
end
it 'filters out test -f commands' do
expect(subject.filter('test -f /tmp/file.txt')).to include(
"$p = \"/tmp/file.txt\"")
expect(subject.filter('test -f /tmp/file.txt')).to include(
"if ((Test-Path $p) -and (!(get-item $p).PSIsContainer)) {")
end
it 'filters out test -x commands' do
expect(subject.filter('test -x /tmp/file.txt')).to include(
"$p = \"/tmp/file.txt\"")
expect(subject.filter('test -x /tmp/file.txt')).to include(
"if ((Test-Path $p) -and (!(get-item $p).PSIsContainer)) {")
end
it 'filters out other test commands' do
expect(subject.filter('test -L /tmp/file.txt')).to include(
"$p = \"/tmp/file.txt\"")
expect(subject.filter('test -L /tmp/file.txt')).to include(
"if (Test-Path $p) {")
end
it 'filters out rm -Rf commands' do
expect(subject.filter('rm -Rf /some/dir')).to eq(
"rm '/some/dir' -recurse -force")
end
it 'filters out rm commands' do
expect(subject.filter('rm /some/dir')).to eq(
"rm '/some/dir' -force")
end
it 'filters out chown commands' do
expect(subject.filter("chown -R root '/tmp/dir'")).to eq('')
end
it 'filters out chmod commands' do
expect(subject.filter("chmod 0600 ~/.ssh/authorized_keys")).to eq('')
end
it 'filters out certain cat commands' do
expect(subject.filter("cat /etc/release | grep -i OmniOS")).to eq('')
end
it 'should not filter out other cat commands' do
expect(subject.filter("cat /tmp/somefile")).to eq('cat /tmp/somefile')
end
end
end

View File

@ -15,19 +15,19 @@ describe VagrantPlugins::CommunicatorWinRM::WinRMShell do
describe ".powershell" do
it "should call winrm powershell" do
expect(session).to receive(:powershell).with("dir").and_return({ exitcode: 0 })
expect(session).to receive(:powershell).with(/^dir.+/).and_return({ exitcode: 0 })
expect(subject.powershell("dir")[:exitcode]).to eq(0)
end
it "should raise auth error when exception message contains 401" do
expect(session).to receive(:powershell).with("dir").and_raise(
expect(session).to receive(:powershell).with(/^dir.+/).and_raise(
StandardError.new("Oh no! a 401 SOAP error!"))
expect { subject.powershell("dir") }.to raise_error(
VagrantPlugins::CommunicatorWinRM::Errors::AuthError)
end
it "should raise an execution error when an exception occurs" do
expect(session).to receive(:powershell).with("dir").and_raise(
expect(session).to receive(:powershell).with(/^dir.+/).and_raise(
StandardError.new("Oh no! a 500 SOAP error!"))
expect { subject.powershell("dir") }.to raise_error(
VagrantPlugins::CommunicatorWinRM::Errors::ExecutionError)