diff --git a/plugins/communicators/winrm/command_filter.rb b/plugins/communicators/winrm/command_filter.rb new file mode 100644 index 000000000..4b5e2ab5c --- /dev/null +++ b/plugins/communicators/winrm/command_filter.rb @@ -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 diff --git a/plugins/communicators/winrm/command_filters/cat.rb b/plugins/communicators/winrm/command_filters/cat.rb new file mode 100644 index 000000000..5d9cf8e01 --- /dev/null +++ b/plugins/communicators/winrm/command_filters/cat.rb @@ -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 diff --git a/plugins/communicators/winrm/command_filters/chmod.rb b/plugins/communicators/winrm/command_filters/chmod.rb new file mode 100644 index 000000000..a336f5dd4 --- /dev/null +++ b/plugins/communicators/winrm/command_filters/chmod.rb @@ -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 diff --git a/plugins/communicators/winrm/command_filters/chown.rb b/plugins/communicators/winrm/command_filters/chown.rb new file mode 100644 index 000000000..127731ff6 --- /dev/null +++ b/plugins/communicators/winrm/command_filters/chown.rb @@ -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 diff --git a/plugins/communicators/winrm/command_filters/grep.rb b/plugins/communicators/winrm/command_filters/grep.rb new file mode 100644 index 000000000..96ce32ca6 --- /dev/null +++ b/plugins/communicators/winrm/command_filters/grep.rb @@ -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 \ No newline at end of file diff --git a/plugins/communicators/winrm/command_filters/rm.rb b/plugins/communicators/winrm/command_filters/rm.rb new file mode 100644 index 000000000..a0078c83d --- /dev/null +++ b/plugins/communicators/winrm/command_filters/rm.rb @@ -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 diff --git a/plugins/communicators/winrm/command_filters/test.rb b/plugins/communicators/winrm/command_filters/test.rb new file mode 100644 index 000000000..23fddc8bf --- /dev/null +++ b/plugins/communicators/winrm/command_filters/test.rb @@ -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 diff --git a/plugins/communicators/winrm/command_filters/uname.rb b/plugins/communicators/winrm/command_filters/uname.rb new file mode 100644 index 000000000..4a8b0836d --- /dev/null +++ b/plugins/communicators/winrm/command_filters/uname.rb @@ -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 diff --git a/plugins/communicators/winrm/command_filters/which.rb b/plugins/communicators/winrm/command_filters/which.rb new file mode 100644 index 000000000..cf046ca19 --- /dev/null +++ b/plugins/communicators/winrm/command_filters/which.rb @@ -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 diff --git a/plugins/communicators/winrm/communicator.rb b/plugins/communicators/winrm/communicator.rb index 850b0eba7..fe1d66f92 100644 --- a/plugins/communicators/winrm/communicator.rb +++ b/plugins/communicators/winrm/communicator.rb @@ -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 diff --git a/plugins/communicators/winrm/scripts/command_alias.ps1 b/plugins/communicators/winrm/scripts/command_alias.ps1 deleted file mode 100644 index 0955db6d8..000000000 --- a/plugins/communicators/winrm/scripts/command_alias.ps1 +++ /dev/null @@ -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 - } -} diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index 772c8944d..131c79141 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -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 diff --git a/test/unit/plugins/communicators/winrm/command_filter_test.rb b/test/unit/plugins/communicators/winrm/command_filter_test.rb new file mode 100644 index 000000000..e56d1cd10 --- /dev/null +++ b/test/unit/plugins/communicators/winrm/command_filter_test.rb @@ -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 diff --git a/test/unit/plugins/communicators/winrm/shell_test.rb b/test/unit/plugins/communicators/winrm/shell_test.rb index 79d6970d8..477108b97 100644 --- a/test/unit/plugins/communicators/winrm/shell_test.rb +++ b/test/unit/plugins/communicators/winrm/shell_test.rb @@ -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)