From f292232ca8a0ea868e06bc86a091dc43160cbce6 Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Tue, 22 Apr 2014 14:42:13 -0700 Subject: [PATCH 1/2] Added Windows guest file manager for recursive uploads - Since WinRM uploads are so slow, MD5 content checking is used --- plugins/communicators/winrm/file_manager.rb | 166 ++++++++++++++++++++ plugins/communicators/winrm/shell.rb | 26 +-- 2 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 plugins/communicators/winrm/file_manager.rb 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 From 5f4a900d8ed04762bbafb3eb05f87d6ccae34b5b Mon Sep 17 00:00:00 2001 From: Shawn Neal Date: Tue, 22 Apr 2014 14:54:13 -0700 Subject: [PATCH 2/2] Add WinRM file transfer error and english text --- plugins/communicators/winrm/errors.rb | 4 ++++ templates/locales/comm_winrm.yml | 6 ++++++ 2 files changed, 10 insertions(+) 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/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}