diff --git a/plugins/communicators/winrm/errors.rb b/plugins/communicators/winrm/errors.rb index ce3ab2066..2285d2115 100644 --- a/plugins/communicators/winrm/errors.rb +++ b/plugins/communicators/winrm/errors.rb @@ -21,6 +21,10 @@ module VagrantPlugins class WinRMNotReady < WinRMError error_key(:winrm_not_ready) end + + class WinRMFileTransferError < WinRMError + error_key(:winrm_file_transfer_error) + end end end end diff --git a/plugins/communicators/winrm/file_manager.rb b/plugins/communicators/winrm/file_manager.rb new file mode 100644 index 000000000..87a87438c --- /dev/null +++ b/plugins/communicators/winrm/file_manager.rb @@ -0,0 +1,166 @@ +require "log4r" + +module VagrantPlugins + module CommunicatorWinRM + + # Manages the file system on the remote guest allowing for file tranfer + # between the guest and host. + class FileManager + + def initialize(shell) + @logger = Log4r::Logger.new("vagrant::communication::filemanager") + @shell = shell + end + + # Uploads the given file or directory from the host to the guest (recursively). + # + # @param [String] The source file or directory path on the host + # @param [String] The destination file or directory path on the host + def upload(host_src_file_path, guest_dest_file_path) + @logger.debug("Upload: #{host_src_file_path} -> #{guest_dest_file_path}") + if File.directory?(host_src_file_path) + upload_directory(host_src_file_path, guest_dest_file_path) + else + upload_file(host_src_file_path, guest_dest_file_path) + end + end + + # Downloads the given file from the guest to the host. + # NOTE: This currently only supports single file download + # + # @param [String] The source file path on the guest + # @param [String] The destination file path on the host + def download(guest_src_file_path, host_dest_file_path) + @logger.debug("#{guest_src_file_path} -> #{host_dest_file_path}") + + output = @shell.powershell("[System.convert]::ToBase64String([System.IO.File]::ReadAllBytes(\"#{guest_src_file_path}\"))") + contents = output[:data].map!{|line| line[:stdout]}.join.gsub("\\n\\r", '') + out = Base64.decode64(contents) + IO.binwrite(host_dest_file_path, out) + end + + + private + + # Recursively uploads the given directory from the host to the guest + # + # @param [String] The source file or directory path on the host + # @param [String] The destination file or directory path on the host + def upload_directory(host_src_file_path, guest_dest_file_path) + glob_patt = File.join(host_src_file_path, '**/*') + Dir.glob(glob_patt).select { |f| !File.directory?(f) }.each do |host_file_path| + guest_file_path = guest_file_path(host_src_file_path, guest_dest_file_path, host_file_path) + upload_file(host_file_path, guest_file_path) + end + end + + # Uploads the given file, but only if the target file doesn't exist + # or its MD5 checksum doens't match the host's source checksum. + # + # @param [String] The source file path on the host + # @param [String] The destination file path on the guest + def upload_file(host_src_file_path, guest_dest_file_path) + if should_upload_file?(host_src_file_path, guest_dest_file_path) + tmp_file_path = upload_to_temp_file(host_src_file_path) + decode_temp_file(tmp_file_path, guest_dest_file_path) + else + @logger.debug("Up to date: #{guest_dest_file_path}") + end + end + + # Uploads the given file to a new temp file on the guest + # + # @param [String] The source file path on the host + # @return [String] The temp file path on the guest + def upload_to_temp_file(host_src_file_path) + tmp_file_path = File.join(guest_temp_dir, "winrm-upload-#{rand()}") + @logger.debug("Uploading '#{host_src_file_path}' to temp file '#{tmp_file_path}'") + + base64_host_file = Base64.encode64(IO.binread(host_src_file_path)).gsub("\n",'') + base64_host_file.chars.to_a.each_slice(8000-tmp_file_path.size) do |chunk| + out = @shell.cmd("echo #{chunk.join} >> \"#{tmp_file_path}\"") + raise_upload_error_if_failed(out, host_src_file_path, tmp_file_path) + end + + tmp_file_path + end + + # Moves and decodes the given file temp file on the guest to its + # permanent location + # + # @param [String] The source base64 encoded temp file path on the guest + # @param [String] The destination file path on the guest + def decode_temp_file(guest_tmp_file_path, guest_dest_file_path) + @logger.debug("Decoding temp file '#{guest_tmp_file_path}' to '#{guest_dest_file_path}'") + out = @shell.powershell <<-EOH + $tmp_file_path = [System.IO.Path]::GetFullPath('#{guest_tmp_file_path}') + $dest_file_path = [System.IO.Path]::GetFullPath('#{guest_dest_file_path}') + + if (Test-Path $dest_file_path) { + rm $dest_file_path + } + else { + $dest_dir = ([System.IO.Path]::GetDirectoryName($dest_file_path)) + New-Item -ItemType directory -Force -Path $dest_dir + } + + $base64_string = Get-Content $tmp_file_path + $bytes = [System.Convert]::FromBase64String($base64_string) + [System.IO.File]::WriteAllBytes($dest_file_path, $bytes) + EOH + raise_upload_error_if_failed(out, guest_tmp_file_path, guest_dest_file_path) + end + + # Checks to see if the target file on the guest is missing or out of date. + # + # @param [String] The source file path on the host + # @param [String] The destination file path on the guest + # @return [Boolean] True if the file is missing or out of date + def should_upload_file?(host_src_file_path, guest_dest_file_path) + local_md5 = Digest::MD5.file(host_src_file_path).hexdigest + cmd = <<-EOH + $dest_file_path = [System.IO.Path]::GetFullPath('#{guest_dest_file_path}') + + if (Test-Path $dest_file_path) { + $crypto_provider = new-object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider + try { + $file = [System.IO.File]::Open($dest_file_path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read) + $guest_md5 = ([System.BitConverter]::ToString($crypto_provider.ComputeHash($file))).Replace("-","").ToLower() + } + finally { + $file.Dispose() + } + if ($guest_md5 -eq '#{local_md5}') { + exit 0 + } + } + exit 1 + EOH + @shell.powershell(cmd)[:exitcode] == 1 + end + + # Creates a guest file path equivalent from a host file path + # + # @param [String] The base host directory we're going to copy from + # @param [String] The base guest directory we're going to copy to + # @param [String] A full path to a file on the host underneath host_base_dir + # @return [String] The guest file path equivalent + def guest_file_path(host_base_dir, guest_base_dir, host_file_path) + rel_path = File.dirname(host_file_path[host_base_dir.length, host_file_path.length]) + File.join(guest_base_dir, rel_path, File.basename(host_file_path)) + end + + def guest_temp_dir + @guest_temp ||= (@shell.cmd('echo %TEMP%'))[:data][0][:stdout].chomp + end + + def raise_upload_error_if_failed(out, from, to) + raise Errors::WinRMFileTransferError, + :from => from, + :to => to, + :message => out.inspect if out[:exitcode] != 0 + end + + end #class + end +end diff --git a/plugins/communicators/winrm/shell.rb b/plugins/communicators/winrm/shell.rb index e1bbb5da0..63edb22af 100644 --- a/plugins/communicators/winrm/shell.rb +++ b/plugins/communicators/winrm/shell.rb @@ -9,6 +9,8 @@ Vagrant::Util::SilenceWarnings.silence! do require "winrm" end +require_relative "file_manager" + module VagrantPlugins module CommunicatorWinRM class WinRMShell @@ -62,31 +64,11 @@ module VagrantPlugins end def upload(from, to) - @logger.debug("Uploading: #{from} to #{to}") - file_name = (cmd("echo %TEMP%\\winrm-upload-#{rand()}"))[:data][0][:stdout].chomp - powershell <<-EOH - if(Test-Path #{to}) { - rm #{to} - } - EOH - Base64.encode64(IO.binread(from)).gsub("\n",'').chars.to_a.each_slice(8000-file_name.size) do |chunk| - out = cmd("echo #{chunk.join} >> \"#{file_name}\"") - end - powershell <<-EOH - mkdir $([System.IO.Path]::GetDirectoryName(\"#{to}\")) - $base64_string = Get-Content \"#{file_name}\" - $bytes = [System.Convert]::FromBase64String($base64_string) - $new_file = [System.IO.Path]::GetFullPath(\"#{to}\") - [System.IO.File]::WriteAllBytes($new_file,$bytes) - EOH + FileManager.new(self).upload(from, to) end def download(from, to) - @logger.debug("Downloading: #{from} to #{to}") - output = powershell("[System.convert]::ToBase64String([System.IO.File]::ReadAllBytes(\"#{from}\"))") - contents = output[:data].map!{|line| line[:stdout]}.join.gsub("\\n\\r", '') - out = Base64.decode64(contents) - IO.binwrite(to, out) + FileManager.new(self).download(from, to) end protected diff --git a/templates/locales/comm_winrm.yml b/templates/locales/comm_winrm.yml index f88e31b2e..0189547f9 100644 --- a/templates/locales/comm_winrm.yml +++ b/templates/locales/comm_winrm.yml @@ -20,3 +20,9 @@ en: The box is not able to report an address for WinRM to connect to yet. WinRM cannot access this Vagrant environment. Please wait for the Vagrant environment to be running and try again. + winrm_file_transfer_error: |- + Failed to transfer a file between the host and guest + + From: %{from} + To: %{to} + Message: %{message}