diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 90e5c42c1..47cd16c23 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -388,6 +388,10 @@ module Vagrant error_key(:hyperv_virtualbox_error) end + class HypervNotSupported < VagrantError + error_key(:hyperv_not_supported) + end + class ForwardPortAdapterNotFound < VagrantError error_key(:forward_port_adapter_not_found) end diff --git a/lib/vagrant/util/hyperv_daemons.rb b/lib/vagrant/util/hyperv_daemons.rb new file mode 100644 index 000000000..221310b53 --- /dev/null +++ b/lib/vagrant/util/hyperv_daemons.rb @@ -0,0 +1,64 @@ +module Vagrant + module Util + module HypervDaemons + HYPERV_DAEMON_SERVICES = %i[kvp vss fcopy] + + def hyperv_daemons_activate(machine) + result = HYPERV_DAEMON_SERVICES.map do |service| + hyperv_daemon_activate machine, service + end + result.all? + end + + def hyperv_daemon_activate(machine, service) + comm = machine.communicate + service_name = hyperv_service_name(machine, service) + return false unless comm.test("systemctl enable #{service_name}", + sudo: true) + + return false unless comm.test("systemctl restart #{service_name}", + sudo: true) + + hyperv_daemon_running machine, service + end + + def hyperv_daemons_running(machine) + result = HYPERV_DAEMON_SERVICES.map do |service| + hyperv_daemon_running machine, service + end + result.all? + end + + def hyperv_daemon_running(machine, service) + comm = machine.communicate + service_name = hyperv_service_name(machine, service) + comm.test("systemctl -q is-active #{service_name}") + end + + def hyperv_daemons_installed(machine) + result = HYPERV_DAEMON_SERVICES.map do |service| + hyperv_daemon_installed machine, service + end + result.all? + end + + def hyperv_daemon_installed(machine, service) + comm = machine.communicate + daemon_name = hyperv_daemon_name(service) + comm.test("which #{daemon_name}") + end + + protected + + def hyperv_service_name(machine, service) + is_deb = machine.communicate.test("which apt-get") + separator = is_deb ? '-' : '_' + ['hv', service.to_s, 'daemon'].join separator + end + + def hyperv_daemon_name(service) + ['hv', service.to_s, 'daemon'].join '_' + end + end + end +end diff --git a/lib/vagrant/util/platform.rb b/lib/vagrant/util/platform.rb index 944ca42ea..997393685 100644 --- a/lib/vagrant/util/platform.rb +++ b/lib/vagrant/util/platform.rb @@ -487,11 +487,11 @@ module Vagrant # # @param [Pathname, String] path Path to convert # @return [String] - def windows_path(path) + def windows_path(path, *args) path = cygwin_windows_path(path) path = wsl_to_windows_path(path) if windows? || wsl? - path = windows_unc_path(path) + path = windows_unc_path(path) if !args.include?(:disable_unc) end path end @@ -651,6 +651,17 @@ module Vagrant @_wsl_windows_appdata_local end + # Fetch the Windows temp directory + # + # @return [String, Nil] + def windows_temp + if !@_windows_temp + result = Vagrant::Util::PowerShell.execute_cmd("(Get-Item Env:TEMP).Value") + @_windows_temp = result.gsub("\"", "").strip + end + @_windows_temp + end + # Confirm Vagrant versions installed within the WSL and the Windows system # are the same. Raise error if they do not match. def wsl_validate_matching_vagrant_versions! diff --git a/plugins/guests/arch/cap/hyperv_daemons.rb b/plugins/guests/arch/cap/hyperv_daemons.rb new file mode 100644 index 000000000..146f912e1 --- /dev/null +++ b/plugins/guests/arch/cap/hyperv_daemons.rb @@ -0,0 +1,19 @@ +module VagrantPlugins + module GuestArch + module Cap + class HypervDaemons + def self.hyperv_daemons_installed(machine) + machine.communicate.test("pacman -Q hyperv") + end + + def self.hyperv_daemons_install(machine) + comm = machine.communicate + comm.sudo <<-EOH.gsub(/^ {12}/, "") + pacman --noconfirm -Syy && + pacman --noconfirm -S hyperv + EOH + end + end + end + end +end diff --git a/plugins/guests/arch/plugin.rb b/plugins/guests/arch/plugin.rb index d35bb84f3..1dd7d3642 100644 --- a/plugins/guests/arch/plugin.rb +++ b/plugins/guests/arch/plugin.rb @@ -45,6 +45,16 @@ module VagrantPlugins require_relative "cap/smb" Cap::SMB end + + guest_capability(:arch, :hyperv_daemons_installed) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + + guest_capability(:arch, :hyperv_daemons_install) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end end end end diff --git a/plugins/guests/bsd/cap/file_system.rb b/plugins/guests/bsd/cap/file_system.rb index c14f7b376..56ba59a32 100644 --- a/plugins/guests/bsd/cap/file_system.rb +++ b/plugins/guests/bsd/cap/file_system.rb @@ -44,7 +44,7 @@ module VagrantPlugins "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] - cmds.each{ |cmd| comm.execute(cmd) } + cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) } true end @@ -68,7 +68,7 @@ module VagrantPlugins "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] - cmds.each{ |cmd| comm.execute(cmd) } + cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) } true end end diff --git a/plugins/guests/debian/cap/hyperv_daemons.rb b/plugins/guests/debian/cap/hyperv_daemons.rb new file mode 100644 index 000000000..6504330c6 --- /dev/null +++ b/plugins/guests/debian/cap/hyperv_daemons.rb @@ -0,0 +1,19 @@ +module VagrantPlugins + module GuestDebian + module Cap + class HypervDaemons + def self.hyperv_daemons_installed(machine) + machine.communicate.test('dpkg -s linux-cloud-tools-common', sudo: true) + end + + def self.hyperv_daemons_install(machine) + comm = machine.communicate + comm.sudo <<-EOH.gsub(/^ {12}/, "") + DEBIAN_FRONTEND=noninteractive apt-get update -y && + apt-get install -y -o Dpkg::Options::="--force-confdef" linux-cloud-tools-common + EOH + end + end + end + end +end diff --git a/plugins/guests/debian/plugin.rb b/plugins/guests/debian/plugin.rb index 705cb6557..32c12fa78 100644 --- a/plugins/guests/debian/plugin.rb +++ b/plugins/guests/debian/plugin.rb @@ -35,6 +35,16 @@ module VagrantPlugins require_relative "cap/smb" Cap::SMB end + + guest_capability(:debian, :hyperv_daemons_installed) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + + guest_capability(:debian, :hyperv_daemons_install) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end end end end diff --git a/plugins/guests/linux/cap/file_system.rb b/plugins/guests/linux/cap/file_system.rb index abff30e37..d5465a9c7 100644 --- a/plugins/guests/linux/cap/file_system.rb +++ b/plugins/guests/linux/cap/file_system.rb @@ -46,7 +46,7 @@ module VagrantPlugins "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] - cmds.each{ |cmd| comm.execute(cmd) } + cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) } true end @@ -70,9 +70,45 @@ module VagrantPlugins "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] - cmds.each{ |cmd| comm.execute(cmd) } + cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) } true end + + # Create directories at given locations on guest + # + # @param [Vagrant::Machine] machine Vagrant guest machine + # @param [array] paths to create on guest + def self.create_directories(machine, dirs, opts={}) + return [] if dirs.empty? + + remote_fn = create_tmp_path(machine, {}) + tmp = Tempfile.new('hv_dirs') + begin + tmp.binmode + tmp.write dirs.join("\n") + "\n" + tmp.close + machine.communicate.upload(tmp.path, remote_fn) + ensure + tmp.close + tmp.unlink + end + created_paths = [] + machine.communicate.execute("bash -c 'while IFS= read -r line + do + if [ ! -z \"${line}\" ] && [ ! -d \"${line}\" ]; then + if [ -f \"${line}\" ]; then + rm \"${line}\" + fi + mkdir -p -v \"${line}\" || true + fi + done < #{remote_fn}' + ", sudo: opts[:sudo] || false) do |type, data| + if type == :stdout && /^.*\'(?.*)\'/ =~ data + created_paths << dir.strip + end + end + created_paths + end end end end diff --git a/plugins/guests/linux/cap/hyperv_daemons.rb b/plugins/guests/linux/cap/hyperv_daemons.rb new file mode 100644 index 000000000..33c1401c6 --- /dev/null +++ b/plugins/guests/linux/cap/hyperv_daemons.rb @@ -0,0 +1,11 @@ +require "vagrant/util/hyperv_daemons" + +module VagrantPlugins + module GuestLinux + module Cap + class HypervDaemons + extend Vagrant::Util::HypervDaemons + end + end + end +end diff --git a/plugins/guests/linux/plugin.rb b/plugins/guests/linux/plugin.rb index 777d5cbec..88d97954b 100644 --- a/plugins/guests/linux/plugin.rb +++ b/plugins/guests/linux/plugin.rb @@ -117,10 +117,30 @@ module VagrantPlugins Cap::RSync end + guest_capability(:linux, :create_directories) do + require_relative "cap/file_system" + Cap::FileSystem + end + guest_capability(:linux, :unmount_virtualbox_shared_folder) do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder end + + guest_capability(:linux, :hyperv_daemons_running) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + + guest_capability(:linux, :hyperv_daemons_activate) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + + guest_capability(:linux, :hyperv_daemons_installed) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end end end end diff --git a/plugins/guests/redhat/cap/hyperv_daemons.rb b/plugins/guests/redhat/cap/hyperv_daemons.rb new file mode 100644 index 000000000..b3e3216be --- /dev/null +++ b/plugins/guests/redhat/cap/hyperv_daemons.rb @@ -0,0 +1,22 @@ +module VagrantPlugins + module GuestRedHat + module Cap + class HypervDaemons + def self.hyperv_daemons_installed(machine) + machine.communicate.test("rpm -q hyperv-daemons") + end + + def self.hyperv_daemons_install(machine) + comm = machine.communicate + comm.sudo <<-EOH.gsub(/^ {12}/, "") + if command -v dnf; then + dnf -y install hyperv-daemons + else + yum -y install hyperv-daemons + fi + EOH + end + end + end + end +end diff --git a/plugins/guests/redhat/plugin.rb b/plugins/guests/redhat/plugin.rb index 8ea37baf4..70d5a8733 100644 --- a/plugins/guests/redhat/plugin.rb +++ b/plugins/guests/redhat/plugin.rb @@ -40,6 +40,16 @@ module VagrantPlugins require_relative "cap/rsync" Cap::RSync end + + guest_capability(:redhat, :hyperv_daemons_installed) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + + guest_capability(:redhat, :hyperv_daemons_install) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end end end end diff --git a/plugins/guests/solaris/cap/file_system.rb b/plugins/guests/solaris/cap/file_system.rb index d697c9db8..7909ce813 100644 --- a/plugins/guests/solaris/cap/file_system.rb +++ b/plugins/guests/solaris/cap/file_system.rb @@ -44,7 +44,7 @@ module VagrantPlugins "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] - cmds.each{ |cmd| comm.execute(cmd) } + cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) } true end @@ -68,7 +68,7 @@ module VagrantPlugins "rm -f '#{compressed_file}'", "rm -rf '#{extract_dir}'" ] - cmds.each{ |cmd| comm.execute(cmd) } + cmds.each{ |cmd| comm.execute(cmd, sudo: opts[:sudo] || false) } true end end diff --git a/plugins/guests/windows/cap/file_system.rb b/plugins/guests/windows/cap/file_system.rb index 85a7d4a2f..c8bc7c53b 100644 --- a/plugins/guests/windows/cap/file_system.rb +++ b/plugins/guests/windows/cap/file_system.rb @@ -1,3 +1,5 @@ +require 'json' + module VagrantPlugins module GuestWindows module Cap @@ -59,6 +61,47 @@ module VagrantPlugins end true end + + # Create directories at given locations on guest + # + # @param [Vagrant::Machine] machine Vagrant guest machine + # @param [array] paths to create on guest + def self.create_directories(machine, dirs, opts={}) + return [] if dirs.empty? + + remote_fn = create_tmp_path(machine, {}) + tmp = Tempfile.new('hv_dirs') + begin + tmp.write dirs.join("\n") + "\n" + tmp.close + machine.communicate.upload(tmp.path, remote_fn) + ensure + tmp.close + tmp.unlink + end + created_paths = [] + cmd = <<-EOH.gsub(/^ {6}/, "") + $files = Get-Content #{remote_fn} + foreach ($file in $files) { + if (-Not (Test-Path($file))) { + ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName) + } else { + if (-Not ((Get-Item $file) -is [System.IO.DirectoryInfo])) { + # Remove the file + Remove-Item -Path $file -Force + ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName) + } + } + } + EOH + machine.communicate.execute(cmd, shell: :powershell) do |type, data| + if type == :stdout + obj = JSON.parse(data) + created_paths << obj["FullName"].strip unless obj["FullName"].nil? + end + end + created_paths + end end end end diff --git a/plugins/guests/windows/cap/hyperv_daemons.rb b/plugins/guests/windows/cap/hyperv_daemons.rb new file mode 100644 index 000000000..9372db29a --- /dev/null +++ b/plugins/guests/windows/cap/hyperv_daemons.rb @@ -0,0 +1,105 @@ +require "vagrant/util/hyperv_daemons" + +module VagrantPlugins + module GuestWindows + module Cap + class HypervDaemons + HYPERV_DAEMON_SERVICES = %i[kvp vss fcopy] + HYPERV_DAEMON_SERVICE_NAMES = {kvp: "vmickvpexchange", vss: "vmicvss", fcopy: "vmicguestinterface" } + + # https://docs.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicecontrollerstatus?view=netframework-4.8 + STOPPED = 1 + START_PENDING = 2 + STOP_PENDING = 3 + RUNNING = 4 + CONTINUE_PENDING = 5 + PAUSE_PENDING = 6 + PAUSED = 7 + + MANUAL_MODE = 3 + DISABLED_MODE = 4 + + def self.hyperv_daemons_activate(machine) + result = HYPERV_DAEMON_SERVICES.map do |service| + hyperv_daemon_activate machine, service + end + result.all? + end + + def self.hyperv_daemon_activate(machine, service) + comm = machine.communicate + service_name = hyperv_service_name(machine, service) + daemon_service = service_info(comm, service_name) + return false if daemon_service.nil? + + if daemon_service["StartType"] == DISABLED_MODE + return false unless enable_service(comm, service_name) + end + + return false unless restart_service(comm, service_name) + hyperv_daemon_running machine, service + end + + def self.hyperv_daemons_running(machine) + result = HYPERV_DAEMON_SERVICES.map do |service| + hyperv_daemon_running machine, service.to_sym + end + result.all? + end + + def self.hyperv_daemon_running(machine, service) + comm = machine.communicate + service_name = hyperv_service_name(machine, service) + daemon_service = service_info(comm, service_name) + return daemon_service["Status"] == RUNNING unless daemon_service.nil? + false + end + + def self.hyperv_daemons_installed(machine) + result = HYPERV_DAEMON_SERVICES.map do |service| + hyperv_daemon_installed machine, service.to_sym + end + result.all? + end + + def self.hyperv_daemon_installed(machine, service) + # Windows guest should have Hyper-V service installed + true + end + + protected + + def self.service_info(comm, service) + cmd = "ConvertTo-Json (Get-Service -Name #{service})" + result = [] + comm.execute(cmd, shell: :powershell) do |type, data| + if type == :stdout + result << JSON.parse(data) + end + end + result[0] || {} + end + + def self.restart_service(comm, service) + cmd = "Restart-Service -Name #{service} -Force" + comm.execute(cmd, shell: :powershell) + true + end + + def self.enable_service(comm, service) + cmd = "Set-Service -Name #{service} -StartupType #{MANUAL_MODE}" + comm.execute(cmd, shell: :powershell) + true + end + + def self.hyperv_service_name(machine, service) + hyperv_daemon_name(service) + end + + def self.hyperv_daemon_name(service) + HYPERV_DAEMON_SERVICE_NAMES[service] + end + end + end + end +end diff --git a/plugins/guests/windows/plugin.rb b/plugins/guests/windows/plugin.rb index a4308d4a2..706202ae2 100644 --- a/plugins/guests/windows/plugin.rb +++ b/plugins/guests/windows/plugin.rb @@ -44,6 +44,11 @@ module VagrantPlugins Cap::FileSystem end + guest_capability(:windows, :create_directories) do + require_relative "cap/file_system" + Cap::FileSystem + end + guest_capability(:windows, :mount_virtualbox_shared_folder) do require_relative "cap/mount_shared_folder" Cap::MountSharedFolder @@ -99,6 +104,21 @@ module VagrantPlugins Cap::PublicKey end + guest_capability(:windows, :hyperv_daemons_running) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + + guest_capability(:windows, :hyperv_daemons_activate) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + + guest_capability(:windows, :hyperv_daemons_installed) do + require_relative "cap/hyperv_daemons" + Cap::HypervDaemons + end + protected def self.init! diff --git a/plugins/providers/hyperv/action.rb b/plugins/providers/hyperv/action.rb index c7e1fb939..502acd444 100644 --- a/plugins/providers/hyperv/action.rb +++ b/plugins/providers/hyperv/action.rb @@ -161,6 +161,15 @@ module VagrantPlugins end end + # This is the action that is called to sync folders to a running + # machine without a reboot. + def self.action_sync_folders + Vagrant::Action::Builder.new.tap do |b| + b.use SyncedFolderCleanup + b.use SyncedFolders + end + end + def self.action_up Vagrant::Action::Builder.new.tap do |b| b.use CheckEnabled diff --git a/plugins/providers/hyperv/command/sync.rb b/plugins/providers/hyperv/command/sync.rb new file mode 100644 index 000000000..a1532e0cd --- /dev/null +++ b/plugins/providers/hyperv/command/sync.rb @@ -0,0 +1,69 @@ +require 'optparse' + +require "vagrant/action/builtin/mixin_synced_folders" + +require_relative "../sync_helper" + +module VagrantPlugins + module HyperV + module Command + class Sync < Vagrant.plugin("2", :command) + include Vagrant::Action::Builtin::MixinSyncedFolders + + def self.synopsis + "syncs synced folders to remote machine" + end + + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant sync [vm-name]" + o.separator "" + o.separator "This command forces any synced folders to sync." + o.separator "Hyper-V currently does not provider an automatic sync so a manual command is used." + o.separator "" + end + + # Parse the options and return if we don't have any target. + argv = parse_options(opts) + return if !argv + + # Go through each machine and perform the rsync + error = false + with_target_vms(argv) do |machine| + if !machine.communicate.ready? + machine.ui.error(I18n.t("vagrant_hyperv.sync.communicator_not_ready")) + error = true + next + end + + # Determine the rsync synced folders for this machine + folders = synced_folders(machine, cached: true)[:hyperv] + next if !folders || folders.empty? + + # short guestpaths first, so we don't step on ourselves + folders = folders.sort_by do |id, data| + if data[:guestpath] + data[:guestpath].length + else + # A long enough path to just do this at the end. + 10000 + end + end + + # Calculate the owner and group + ssh_info = machine.ssh_info + + # Sync them! + folders.each do |id, data| + next unless data[:guestpath] + + SyncHelper.sync_single machine, ssh_info, data + end + end + + return error ? 1 : 0 + end + end + end + end +end diff --git a/plugins/providers/hyperv/command/sync_auto.rb b/plugins/providers/hyperv/command/sync_auto.rb new file mode 100644 index 000000000..d2c882200 --- /dev/null +++ b/plugins/providers/hyperv/command/sync_auto.rb @@ -0,0 +1,196 @@ +require "log4r" +require 'optparse' +require "thread" + +require "vagrant/action/builtin/mixin_synced_folders" +require "vagrant/util/busy" +require "vagrant/util/platform" + +require "listen" + +require_relative '../sync_helper' + +module VagrantPlugins + module HyperV + module Command + class SyncAuto < Vagrant.plugin("2", :command) + include Vagrant::Action::Builtin::MixinSyncedFolders + + def self.synopsis + "syncs synced folders automatically when files change" + end + + def execute + @logger = Log4r::Logger.new("vagrant::commands::sync-auto") + + options = {} + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant sync-auto [vm-name]" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--[no-]poll", "Force polling filesystem (slow)") do |poll| + options[:poll] = poll + end + end + + # Parse the options and return if we don't have any target. + argv = parse_options(opts) + return if !argv + + # Build up the paths that we need to listen to. + paths = {} + ignores = [] + with_target_vms(argv) do |machine| + next if machine.state.id == :not_created + + cached = synced_folders(machine, cached: true) + fresh = synced_folders(machine) + diff = synced_folders_diff(cached, fresh) + if !diff[:added].empty? + machine.ui.warn(I18n.t("vagrant_hyperv.sync.auto_new_folders")) + end + + folders = cached[:hyperv] + next if !folders || folders.empty? + + # Get the SSH info for this machine so we can do an initial + # sync to the VM. + ssh_info = machine.ssh_info + if ssh_info + machine.ui.info(I18n.t("vagrant_hyperv.sync.auto_initial")) + folders.each do |id, data| + next unless data[:guestpath] + + SyncHelper.sync_single machine, ssh_info, data + end + end + + folders.each do |id, folder_opts| + # If we marked this folder to not auto sync, then + # don't do it. + next if folder_opts.key?(:auto) && !folder_opts[:auto] + + hostpath = folder_opts[:hostpath] + expanded_hostpath = HyperV::SyncHelper.expand_path(hostpath, machine.env.root_path) + paths[expanded_hostpath] ||= [] + paths[expanded_hostpath] << { + id: id, + machine: machine, + opts: folder_opts, + } + + excludes = HyperV::SyncHelper.expand_excludes(hostpath, folder_opts[:exclude]) + excludes[:dirs].each do |dir| + dir = dir.gsub File.join(expanded_hostpath, ''), '' + dir = dir.gsub '.', '\.' + ignores << Regexp.new("#{dir}.*") + end + end + end + + # Exit immediately if there is nothing to watch + if paths.empty? + @env.ui.info(I18n.t("vagrant_hyperv.sync.auto_no_paths")) + return 1 + end + + # Output to the user what paths we'll be watching + paths.keys.sort.each do |path| + paths[path].each do |path_opts| + path_opts[:machine].ui.info(I18n.t( + "vagrant_hyperv.sync.auto_path", + path: path.to_s, + )) + end + end + + @logger.info("Listening to paths: #{paths.keys.sort.inspect}") + @logger.info("Ignoring #{ignores.length} paths:") + ignores.each do |ignore| + @logger.info(" -- #{ignore.to_s}") + end + @logger.info("Listening via: #{Listen::Adapter.select.inspect}") + callback = method(:callback).to_proc.curry[paths] + listopts = { ignore: ignores, force_polling: !!options[:poll] } + listener = Listen.to(*paths.keys, listopts, &callback) + + # Create the callback that lets us know when we've been interrupted + queue = Queue.new + callback = lambda do + # This needs to execute in another thread because Thread + # synchronization can't happen in a trap context. + Thread.new { queue << true } + end + + # Run the listener in a busy block so that we can cleanly + # exit once we receive an interrupt. + Vagrant::Util::Busy.busy(callback) do + listener.start + queue.pop + listener.stop if listener.state != :stopped + end + + 0 + end + + # This is the callback that is called when any changes happen + def callback(paths, modified, added, removed) + @logger.info("File change callback called!") + @logger.info(" - Modified: #{modified.inspect}") + @logger.info(" - Added: #{added.inspect}") + @logger.info(" - Removed: #{removed.inspect}") + + tosync = [] + paths.each do |hostpath, folders| + # Find out if this path should be synced + found = catch(:done) do + [modified, added, removed].each do |changed| + changed.each do |listenpath| + throw :done, true if listenpath.start_with?(hostpath) + end + end + + # Make sure to return false if all else fails so that we + # don't sync to this machine. + false + end + + # If it should be synced, store it for later + tosync << folders if found + end + + # Sync all the folders that need to be synced + tosync.each do |folders| + folders.each do |opts| + # Reload so we get the latest ID + opts[:machine].reload + if !opts[:machine].id || opts[:machine].id == "" + # Skip since we can't get SSH info without an ID + next + end + + ssh_info = opts[:machine].ssh_info + begin + start = Time.now + SyncHelper.sync_single opts[:machine], ssh_info, opts[:opts] + finish = Time.now + @logger.info("Time spent in sync: #{finish - start} (in seconds)") + rescue Vagrant::Errors::MachineGuestNotReady + # Error communicating to the machine, probably a reload or + # halt is happening. Just notify the user but don't fail out. + opts[:machine].ui.error(I18n.t( + "vagrant_hyperv.sync.communicator_not_ready_callback")) + rescue Vagrant::Errors::VagrantError => e + # Error auto sync folder, show an error + opts[:machine].ui.error(I18n.t( + "vagrant_hyperv.sync.auto_sync_error", message: e.to_s)) + end + end + end + end + end + end + end +end diff --git a/plugins/providers/hyperv/driver.rb b/plugins/providers/hyperv/driver.rb index e200a8224..cde0b7ddb 100644 --- a/plugins/providers/hyperv/driver.rb +++ b/plugins/providers/hyperv/driver.rb @@ -1,6 +1,10 @@ +require 'fileutils' +require 'find' require "json" +require 'tempfile' require "vagrant/util/powershell" +require "vagrant/util/subprocess" require_relative "plugin" @@ -120,7 +124,7 @@ module VagrantPlugins # # @return [nil] def start - execute(:start_vm, VmId: vm_id ) + execute(:start_vm, VmId: vm_id) end # Stop the VM @@ -217,6 +221,36 @@ module VagrantPlugins execute(:set_name, VMID: vm_id, VMName: vmname) end + # Sync files + # + # @return [nil] + def sync_files(vm_id, dirs, files, is_win_guest: true) + network_info = read_guest_ip + guest_ip = network_info["ip"] + suffix = (0...8).map { ('a'..'z').to_a[rand(26)] }.join + windows_temp = Vagrant::Util::Platform.windows_temp + if Vagrant::Util::Platform.wsl? + process = Vagrant::Util::Subprocess.execute( + "wslpath", "-u", "-a", windows_temp) + windows_temp = process.stdout.chomp if process.exit_code == 0 + end + fn = File.join(windows_temp, ".hv_sync_files_#{suffix}") + begin + File.open(fn, 'w') do |file| + file.write files.to_json + end + win_path = Vagrant::Util::Platform.windows_path( + fn, :disable_unc) + status = execute(:sync_files, + vm_id: vm_id, + guest_ip: guest_ip, + file_list: win_path) + status + ensure + FileUtils.rm_f(fn) + end + end + protected def execute_powershell(path, options, &block) diff --git a/plugins/providers/hyperv/plugin.rb b/plugins/providers/hyperv/plugin.rb index 484bf2cae..c6ffd5233 100644 --- a/plugins/providers/hyperv/plugin.rb +++ b/plugins/providers/hyperv/plugin.rb @@ -16,6 +16,11 @@ module VagrantPlugins Provider end + synced_folder(:hyperv) do + require File.expand_path("../synced_folder", __FILE__) + SyncedFolder + end + config(:hyperv, :provider) do require_relative "config" init! @@ -32,6 +37,16 @@ module VagrantPlugins Cap::SnapshotList end + command("hyperv-sync", primary: false) do + require_relative "command/sync" + Command::Sync + end + + command("hyperv-sync-auto", primary: false) do + require_relative "command/sync_auto" + Command::SyncAuto + end + protected def self.init! diff --git a/plugins/providers/hyperv/scripts/sync_files.ps1 b/plugins/providers/hyperv/scripts/sync_files.ps1 new file mode 100644 index 000000000..5d12d2e54 --- /dev/null +++ b/plugins/providers/hyperv/scripts/sync_files.ps1 @@ -0,0 +1,48 @@ +#Requires -Modules VagrantMessages +#------------------------------------------------------------------------- +# Copyright (c) 2019 Microsoft +# All Rights Reserved. Licensed under the MIT License. +#-------------------------------------------------------------------------- + +param ( + [parameter (Mandatory=$true)] + [string]$vm_id, + [parameter (Mandatory=$true)] + [string]$guest_ip, + [parameter (Mandatory=$true)] + [string]$file_list, + [string]$path_separator +) + +function copy-file($machine, $file_list, $path_separator) { + $files = Get-Content $file_list | ConvertFrom-Json + $succeeded = @() + $failed = @() + foreach ($line in $files.PSObject.Properties) { + $from = $sourceDir = $line.Name + $to = $destDir = $line.Value + Write-Host "Copying $from to $($machine) => $to..." + Try { + Hyper-V\Copy-VMFile -VM $machine -SourcePath $from -DestinationPath $to -CreateFullPath -FileSource Host -Force + $succeeded += $from + Write-Host "Copied $from to $($machine) => $to." + } Catch { + $failed += $from + } + } + [hashtable]$return = @{} + $return.succeeded = $succeeded + $return.failed = $failed + return $return +} + +$machine = Hyper-V\Get-VM -Id $vm_id + +$status = copy-file $machine $file_list $path_separator + +$resultHash = @{ + message = "OK" + status = $status +} +$result = ConvertTo-Json $resultHash +Write-OutputMessage $result diff --git a/plugins/providers/hyperv/sync_helper.rb b/plugins/providers/hyperv/sync_helper.rb new file mode 100644 index 000000000..003a625c7 --- /dev/null +++ b/plugins/providers/hyperv/sync_helper.rb @@ -0,0 +1,343 @@ +require "vagrant/util/platform" + +module VagrantPlugins + module HyperV + class SyncHelper + WINDOWS_SEPARATOR = "\\" + UNIX_SEPARATOR = "/" + + # Expands glob-style exclude string + # + # @param [String] path Path to operate on + # @param [String] exclude Array of glob-style exclude strings + # @return [Hash] Excluded directories and files + def self.expand_excludes(path, exclude) + excludes = ['.vagrant/'] + excludes += Array(exclude).map(&:to_s) if exclude + excludes.uniq! + + expanded_path = expand_path(path) + excluded_dirs = [] + excluded_files = [] + excludes.map do |exclude| + # Dir.glob accepts Unix style path only + excluded_path = platform_join expanded_path, exclude, is_windows: false + Dir.glob(excluded_path) do |e| + if directory?(e) + excluded_dirs << e + else + excluded_files << e + end + end + end + {dirs: excluded_dirs, + files: excluded_files} + end + + def self.find_includes(path, exclude) + expanded_path = expand_path(path) + excludes = expand_excludes(path, exclude) + included_dirs = [] + included_files = [] + Find.find(expanded_path) do |e| + if directory?(e) + path = File.join e, '' + next if excludes[:dirs].include? path + next if excludes[:dirs].select { |x| path.start_with? x }.any? + + included_dirs << e + else + next if excludes[:files].include? e + next if excludes[:dirs].select { |x| e.start_with? x }.any? + + included_files << e + end + end + { dirs: included_dirs, + files: included_files } + end + + def self.path_mapping(host_path, guest_path, includes, is_win_guest:) + host_path = expand_path(host_path) + platform_host_path = platform_path host_path, is_windows: !Vagrant::Util::Platform.wsl? + win_host_path = Vagrant::Util::Platform.windows_path(host_path, :disable_unc) + platform_guest_path = platform_path(guest_path, is_windows: is_win_guest) + + dir_mappings = { hyperv: {}, platform: {} } + file_mappings = { hyperv: {}, platform: {} } + { dirs: dir_mappings, + files: file_mappings }.map do |sym, mapping| + includes[sym].map do |e| + guest_rel = e.gsub(host_path, '') + guest_rel = trim_head guest_rel + guest_rel = to_unix_path guest_rel + + if guest_rel == '' + file_host_path = win_host_path + file_platform_host_path = platform_host_path + target = platform_guest_path + else + file_host_path = platform_join(win_host_path, guest_rel) + file_platform_host_path = platform_join(platform_host_path, guest_rel, + is_windows: !Vagrant::Util::Platform.wsl?) + guest_rel = guest_rel.split(UNIX_SEPARATOR)[0..-2].join(UNIX_SEPARATOR) if sym == :files + target = platform_join(platform_guest_path, guest_rel, is_windows: is_win_guest) + target = trim_tail target + end + # make sure the dir names are Windows-style for them to pass to Hyper-V + mapping[:hyperv][file_host_path] = target + mapping[:platform][file_platform_host_path] = target + end + end + { dirs: dir_mappings, files: file_mappings } + end + + # Syncs single folder to guest machine + # + # @param [Vagrant::Machine] path Path to operate on + # @param [Hash] ssh_info + # @param [Hash] opts Synced folder details + def self.sync_single(machine, ssh_info, opts) + is_win_guest = machine.guest.name == :windows + host_path = opts[:hostpath] + guest_path = opts[:guestpath] + + includes = find_includes(host_path, opts[:exclude]) + if opts[:no_compression] + # Copy file to guest directly for disk consumption saving + guest_path_mapping = path_mapping(host_path, guest_path, includes, is_win_guest: is_win_guest) + remove_directory machine, guest_path, is_win_guest: is_win_guest, sudo: true + machine.guest.capability(:create_directories, guest_path_mapping[:dirs][:hyperv].values, sudo: true) + if hyperv_copy? machine + machine.provider.driver.sync_files(machine.id, + guest_path_mapping[:dirs][:hyperv], + guest_path_mapping[:files][:hyperv], + is_win_guest: is_win_guest) + else + guest_path_mapping[:files][:platform].each do |host_path, guest_path| + next unless file_exist? host_path + + stat = file_stat host_path + next if stat.symlink? + + machine.communicate.upload(host_path, guest_path) + end + end + else + source_items = includes[:files] + type = is_win_guest ? :zip : :tgz + host_path = expand_path(host_path) + source = send("compress_source_#{type}".to_sym, host_path, source_items) + decompress_cap = type == :zip ? :decompress_zip : :decompress_tgz + begin + destination = machine.guest.capability(:create_tmp_path, extension: ".#{type}") + upload_file(machine, source, destination, is_win_guest: is_win_guest) + remove_directory machine, guest_path, is_win_guest: is_win_guest, sudo: true + machine.guest.capability(decompress_cap, destination, platform_path(guest_path, is_windows: is_win_guest), + type: :directory, sudo: true) + ensure + FileUtils.rm_f source if file_exist? source + end + end + end + + # Compress path using zip into temporary file + # + # @param [String] path Path to compress + # @return [String] path to compressed file + def self.compress_source_zip(path, source_items) + require "zip" + zipfile = Tempfile.create(%w(vagrant .zip), format_windows_temp) + zipfile.close + c_dir = nil + Zip::File.open(zipfile.path, Zip::File::CREATE) do |zip| + source_items.each do |source_item| + next unless file_exist? source_item + next if directory?(source_item) + + stat = file_stat(source_item) + next if stat.symlink? + + trim_item = source_item.sub(path, "").sub(%r{^[/\\]}, "") + dirname = File.dirname(trim_item) + begin + zip.get_entry(dirname) + rescue Errno::ENOENT + zip.mkdir dirname if c_dir != dirname + end + c_dir = dirname + zip.get_output_stream(trim_item) do |f| + source_file = File.open(source_item, "rb") + while data = source_file.read(2048) + f.write(data) + end + end + end + end + zipfile.path + end + + # Compress path using tar and gzip into temporary file + # + # @param [String] path Path to compress + # @return [String] path to compressed file + def self.compress_source_tgz(path, source_items) + tmp_dir = format_windows_temp + tarfile = Tempfile.create(%w(vagrant .tar), tmp_dir) + tarfile.close + tarfile = File.open(tarfile.path, "wb+") + tgzfile = Tempfile.create(%w(vagrant .tgz), tmp_dir) + tgzfile.close + tgzfile = File.open(tgzfile.path, "wb") + tar = Gem::Package::TarWriter.new(tarfile) + tgz = Zlib::GzipWriter.new(tgzfile) + source_items.each do |item| + next unless file_exist? item + + rel_path = item.sub(path, "") + stat = file_stat(item) + item_mode = stat.mode + + if directory?(item) + tar.mkdir(rel_path, item_mode) + elsif stat.symlink? + tar.add_symlink(rel_path, File.readlink(item), item_mode) + else + tar.add_file(rel_path, item_mode) do |io| + File.open(item, "rb") do |file| + while bytes = file.read(4096) + io.write(bytes) + end + end + end + end + end + tar.close + tarfile.rewind + while bytes = tarfile.read(4096) + tgz.write bytes + end + tgz.close + tgzfile.close + tarfile.close + File.delete(tarfile.path) + tgzfile.path + end + + def self.remove_directory(machine, guestpath, is_win_guest: false, sudo: false) + comm = machine.communicate + if is_win_guest + guestpath = to_windows_path guestpath + cmd = <<-EOH.gsub(/^ {6}/, "") + if (Test-Path(\"#{guestpath}\")) { + Remove-Item -Path \"#{guestpath}\" -Recurse -Force + } + EOH + comm.execute(cmd, shell: :powershell) + else + guestpath = to_unix_path guestpath + if comm.test("test -d '#{guestpath}'") + comm.execute("rm -rf '#{guestpath}'", sudo: sudo) + end + end + end + + def self.format_windows_temp + windows_temp = Vagrant::Util::Platform.windows_temp + if Vagrant::Util::Platform.wsl? + process = Vagrant::Util::Subprocess.execute( + "wslpath", "-u", "-a", windows_temp) + windows_temp = process.stdout.chomp if process.exit_code == 0 + end + windows_temp + end + + def self.upload_file(machine, source, dest, is_win_guest:) + begin + # try Hyper-V guest integration service first as WinRM upload is slower + if hyperv_copy? machine + separator = is_win_guest ? WINDOWS_SEPARATOR: UNIX_SEPARATOR + parts = dest.split(separator) + filename = parts[-1] + dest_dir = parts[0..-2].join(separator) + + windows_temp = format_windows_temp + source_copy = platform_join windows_temp, filename, is_windows: !Vagrant::Util::Platform.wsl? + FileUtils.mv source, source_copy + source = source_copy + win_source_path = Vagrant::Util::Platform.windows_path(source, :disable_unc) + hyperv_copy machine, win_source_path, dest_dir + else + machine.communicate.upload(source, dest) + end + ensure + FileUtils.rm_f source + end + end + + def self.hyperv_copy?(machine) + machine.guest.capability?(:hyperv_daemons_running) && machine.guest.capability(:hyperv_daemons_running) + end + + def self.hyperv_copy(machine, source, dest_dir) + vm_id = machine.id + ps_cmd = <<-EOH.gsub(/^ {6}/, "") + $machine = Hyper-V\\Get-VM -Id \"#{vm_id}\" + Hyper-V\\Copy-VMFile -VM $machine -SourcePath \"#{source}\" -DestinationPath \"#{dest_dir}\" -CreateFullPath -FileSource Host -Force + EOH + Vagrant::Util::PowerShell.execute_cmd(ps_cmd) + end + + def self.platform_join(string, *smth, is_windows: true) + joined = [string, *smth].join is_windows ? WINDOWS_SEPARATOR : UNIX_SEPARATOR + if is_windows + to_windows_path joined + else + to_unix_path joined + end + end + + def self.platform_path(path, is_windows: true) + win_path = to_windows_path path + linux_path = to_unix_path path + is_windows ? win_path : linux_path + end + + def self.expand_path(*path) + # stub for unit test + File.expand_path(*path) + end + + def self.directory?(path) + # stub for unit test + File.directory? path + end + + def self.file_exist?(path) + # stub for unit test + File.exist? path + end + + def self.file_stat(path) + # stub for unit test + File.stat path + end + + def self.to_windows_path(path) + path.tr UNIX_SEPARATOR, WINDOWS_SEPARATOR + end + + def self.to_unix_path(path) + path.tr WINDOWS_SEPARATOR, UNIX_SEPARATOR + end + + def self.trim_head(path) + path.start_with?(WINDOWS_SEPARATOR, UNIX_SEPARATOR) ? path[1..-1] : path + end + + def self.trim_tail(path) + path.end_with?(WINDOWS_SEPARATOR, UNIX_SEPARATOR) ? path[0..-2] : path + end + end + end +end diff --git a/plugins/providers/hyperv/synced_folder.rb b/plugins/providers/hyperv/synced_folder.rb new file mode 100644 index 000000000..64886c1c3 --- /dev/null +++ b/plugins/providers/hyperv/synced_folder.rb @@ -0,0 +1,96 @@ +require "fileutils" +require "vagrant/util/platform" + +require_relative 'sync_helper' + +module VagrantPlugins + module HyperV + class SyncedFolder < Vagrant.plugin("2", :synced_folder) + def usable?(machine, raise_errors=false) + # These synced folders only work if the provider if VirtualBox + return false if machine.provider_name != :hyperv + + true + end + + def prepare(machine, folders, _opts) + # Nothing is necessary to do before VM boot. + end + + def enable(machine, folders, _opts) + machine.ui.warn I18n.t("vagrant_hyperv.share_folders.no_daemons") unless configure_hv_daemons(machine) + + # short guestpaths first, so we don't step on ourselves + folders = folders.sort_by do |id, data| + if data[:guestpath] + data[:guestpath].length + else + # A long enough path to just do this at the end. + 10000 + end + end + + # Go through each folder and mount + machine.ui.output(I18n.t("vagrant_hyperv.share_folders.syncing")) + folders.each do |id, data| + if data[:guestpath] + # Guest path specified, so sync the folder to specified point + machine.ui.detail(I18n.t("vagrant_hyperv.share_folders.syncing_entry", + guestpath: data[:guestpath], + hostpath: data[:hostpath])) + + # Dup the data so we can pass it to the guest API + SyncHelper.sync_single machine, machine.ssh_info, data.dup + else + # If no guest path is specified, then automounting is disabled + machine.ui.detail(I18n.t("vagrant_hyperv.share_folders.nosync_entry", + hostpath: data[:hostpath])) + end + end + end + + def disable(machine, folders, _opts) end + + def cleanup(machine, opts) end + + protected + + def configure_hv_daemons(machine) + return false unless machine.guest.capability?(:hyperv_daemons_running) + + unless machine.guest.capability(:hyperv_daemons_running) + installed = machine.guest.capability(:hyperv_daemons_installed) + unless installed + can_install = machine.guest.capability?(:hyperv_daemons_install) + unless can_install + machine.ui.warn I18n.t("vagrant_hyperv.daemons.unable_to_install") + return false + end + + machine.ui.info I18n.t("vagrant_hyperv.daemons.installing") + machine.guest.capability(:hyperv_daemons_install) + end + + can_activate = machine.guest.capability?(:hyperv_daemons_activate) + unless can_activate + machine.ui.warn I18n.t("vagrant_hyperv.daemons.unable_to_activate") + return false + end + + machine.ui.info I18n.t("vagrant_hyperv.daemons.activating") + activated = machine.guest.capability(:hyperv_daemons_activate) + unless activated + machine.ui.warn I18n.t("vagrant_hyperv.daemons.activation_failed") + return false + end + end + true + end + + # This is here so that we can stub it for tests + def driver(machine) + machine.provider.driver + end + end + end +end diff --git a/templates/locales/providers_hyperv.yml b/templates/locales/providers_hyperv.yml index dd2a13c44..a9df7997a 100644 --- a/templates/locales/providers_hyperv.yml +++ b/templates/locales/providers_hyperv.yml @@ -10,6 +10,52 @@ en: VM not created. Moving on... message_not_running: |- Hyper-V machine isn't running. Can't SSH in! + share_folders: + syncing: Syncing shared folders... + syncing_entry: "%{guestpath} => %{hostpath}" + nosync_entry: "Sync disabled: %{hostpath}" + no_daemons: |- + Hyper-V daemons are not running on the guest operation system. + This may impact the performance. + daemons: + installing: |- + Installing Hyper-V daemons... + activating: |- + Activating Hyper-V daemons... + unable_to_install: |- + Hyper-V daemons cannot be installed in the guest operation system. + unable_to_activate: |- + Hyper-V daemons cannot be activated in the guest operation system + as no Hyper-V daemons activation approach is provided on the guest + operation system. + activation_failed: |- + Failed to activate in the guest operation system. + sync: + auto_initial: |- + Doing an initial sync... + auto_new_folders: |- + New synced folders were added to the Vagrantfile since running + `vagrant reload`. If these new synced folders are backed by Hyper-V + auto sync, they won't be automatically synced until a + `vagrant reload` is run. + auto_no_paths: |- + There are no paths to watch! This is either because you have no + synced folders, or any synced folders you have have specified `auto` + to be false. + auto_path: "Watching: %{path}" + auto_sync_error: |- + There was an error while syncing. The error is shown below. + This may not be critical since syncing sometimes fails, but if this + message repeats, then please fix the issue: + + %{message} + communicator_not_ready: |- + The machine is reporting that it is not ready to communicate with it. + Verify that this machine is properly running. + communicator_not_ready_callback: |- + Failed to connect to remote machine. This is usually caused by the + machine rebooting or being halted. Please make sure the machine is + running, and modify a file to try again. config: invalid_auto_start_action: |- diff --git a/test/unit/plugins/guests/arch/cap/hyperv_daemons_test.rb b/test/unit/plugins/guests/arch/cap/hyperv_daemons_test.rb new file mode 100644 index 000000000..7edf9d42f --- /dev/null +++ b/test/unit/plugins/guests/arch/cap/hyperv_daemons_test.rb @@ -0,0 +1,44 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestArch::Cap::HypervDaemons" do + let(:caps) do + VagrantPlugins::GuestArch::Plugin + .components + .guest_capabilities[:arch] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".hyperv_daemons_installed" do + let(:cap) { caps.get(:hyperv_daemons_install) } + + it "checks whether hyperv package is installed" do + cap.hyperv_daemons_installed(machine) + expect(comm.received_commands[0]).to match(/pacman -Q hyperv/) + end + end + + describe ".hyperv_daemons_install" do + let(:cap) { caps.get(:hyperv_daemons_install) } + let(:cmd) do + <<-EOH.gsub(/^ {12}/, "") + pacman --noconfirm -Syy && + pacman --noconfirm -S hyperv + EOH + end + + it "installs hyperv package" do + cap.hyperv_daemons_install(machine) + expect(comm.received_commands[0]).to match("pacman --noconfirm -Syy &&\npacman --noconfirm -S hyperv\n") + end + end +end diff --git a/test/unit/plugins/guests/bsd/cap/file_system_test.rb b/test/unit/plugins/guests/bsd/cap/file_system_test.rb index 7e7d528d6..c956e71d4 100644 --- a/test/unit/plugins/guests/bsd/cap/file_system_test.rb +++ b/test/unit/plugins/guests/bsd/cap/file_system_test.rb @@ -45,40 +45,45 @@ describe "VagrantPlugins::GuestBSD::Cap::FileSystem" do let(:cap) { caps.get(:decompress_tgz) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } - let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_tgz(machine, comp, dest, opts) } - it "should create temporary directory for extraction" do - expect(cap).to receive(:create_tmp_path) - end + [false, true].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + let(:opts) { {sudo: sudo_flag} } - it "should extract file with tar" do - expect(comm).to receive(:execute).with(/tar/) - end + it "should create temporary directory for extraction" do + expect(cap).to receive(:create_tmp_path) + end - it "should extract file to temporary directory" do - expect(comm).to receive(:execute).with(/TMP_DIR/) - end + it "should extract file with tar" do + expect(comm).to receive(:execute).with(/tar/, sudo: sudo_flag) + end - it "should remove compressed file from guest" do - expect(comm).to receive(:execute).with(/rm .*#{comp}/) - end + it "should extract file to temporary directory" do + expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag) + end - it "should remove extraction directory from guest" do - expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) - end + it "should remove compressed file from guest" do + expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag) + end - it "should create parent directories for destination" do - expect(comm).to receive(:execute).with(/mkdir -p .*to'/) - end + it "should remove extraction directory from guest" do + expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag) + end - context "when type is directory" do - before { opts[:type] = :directory } + it "should create parent directories for destination" do + expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag) + end - it "should create destination directory" do - expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) + context "when type is directory" do + before { opts[:type] = :directory } + + it "should create destination directory" do + expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag) + end + end end end end @@ -87,40 +92,45 @@ describe "VagrantPlugins::GuestBSD::Cap::FileSystem" do let(:cap) { caps.get(:decompress_zip) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } - let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_zip(machine, comp, dest, opts) } - it "should create temporary directory for extraction" do - expect(cap).to receive(:create_tmp_path) - end + [false, true].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + let(:opts) { {sudo: sudo_flag} } - it "should extract file with zip" do - expect(comm).to receive(:execute).with(/zip/) - end + it "should create temporary directory for extraction" do + expect(cap).to receive(:create_tmp_path) + end - it "should extract file to temporary directory" do - expect(comm).to receive(:execute).with(/TMP_DIR/) - end + it "should extract file with zip" do + expect(comm).to receive(:execute).with(/zip/, sudo: sudo_flag) + end - it "should remove compressed file from guest" do - expect(comm).to receive(:execute).with(/rm .*#{comp}/) - end + it "should extract file to temporary directory" do + expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag) + end - it "should remove extraction directory from guest" do - expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) - end + it "should remove compressed file from guest" do + expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag) + end - it "should create parent directories for destination" do - expect(comm).to receive(:execute).with(/mkdir -p .*to'/) - end + it "should remove extraction directory from guest" do + expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag) + end - context "when type is directory" do - before { opts[:type] = :directory } + it "should create parent directories for destination" do + expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag) + end - it "should create destination directory" do - expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) + context "when type is directory" do + before { opts[:type] = :directory } + + it "should create destination directory" do + expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag) + end + end end end end diff --git a/test/unit/plugins/guests/debian/cap/hyperv_daemons_test.rb b/test/unit/plugins/guests/debian/cap/hyperv_daemons_test.rb new file mode 100644 index 000000000..9c9cd991e --- /dev/null +++ b/test/unit/plugins/guests/debian/cap/hyperv_daemons_test.rb @@ -0,0 +1,44 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestDebian::Cap::HypervDaemons" do + let(:caps) do + VagrantPlugins::GuestDebian::Plugin + .components + .guest_capabilities[:debian] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".hyperv_daemons_installed" do + let(:cap) { caps.get(:hyperv_daemons_installed) } + + it "checks whether linux-cloud-tools-common package is installed" do + cap.hyperv_daemons_installed(machine) + expect(comm.received_commands[0]).to match(/dpkg -s linux-cloud-tools-common/) + end + end + + describe ".hyperv_daemons_install" do + let(:cap) { caps.get(:hyperv_daemons_install) } + let(:cmd) do + <<-EOH.gsub(/^ {12}/, "") + DEBIAN_FRONTEND=noninteractive apt-get update -y && + apt-get install -y -o Dpkg::Options::="--force-confdef" linux-cloud-tools-common + EOH + end + + it "install linux-cloud-tools-common package" do + cap.hyperv_daemons_install(machine) + expect(comm.received_commands[0]).to match("DEBIAN_FRONTEND=noninteractive apt-get update -y &&\napt-get install -y -o Dpkg::Options::=\"--force-confdef\" linux-cloud-tools-common\n") + end + end +end diff --git a/test/unit/plugins/guests/linux/cap/file_system_test.rb b/test/unit/plugins/guests/linux/cap/file_system_test.rb index aa7c0340c..e8c7f7c8e 100644 --- a/test/unit/plugins/guests/linux/cap/file_system_test.rb +++ b/test/unit/plugins/guests/linux/cap/file_system_test.rb @@ -45,40 +45,45 @@ describe "VagrantPlugins::GuestLinux::Cap::FileSystem" do let(:cap) { caps.get(:decompress_tgz) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } - let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_tgz(machine, comp, dest, opts) } - it "should create temporary directory for extraction" do - expect(cap).to receive(:create_tmp_path) - end + [false, true].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + let(:opts) { {sudo: sudo_flag} } - it "should extract file with tar" do - expect(comm).to receive(:execute).with(/tar/) - end + it "should create temporary directory for extraction" do + expect(cap).to receive(:create_tmp_path) + end - it "should extract file to temporary directory" do - expect(comm).to receive(:execute).with(/TMP_DIR/) - end + it "should extract file with tar" do + expect(comm).to receive(:execute).with(/tar/, sudo: sudo_flag) + end - it "should remove compressed file from guest" do - expect(comm).to receive(:execute).with(/rm .*#{comp}/) - end + it "should extract file to temporary directory" do + expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag) + end - it "should remove extraction directory from guest" do - expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) - end + it "should remove compressed file from guest" do + expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag) + end - it "should create parent directories for destination" do - expect(comm).to receive(:execute).with(/mkdir -p .*to'/) - end + it "should remove extraction directory from guest" do + expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag) + end - context "when type is directory" do - before { opts[:type] = :directory } + it "should create parent directories for destination" do + expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag) + end - it "should create destination directory" do - expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) + context "when type is directory" do + before { opts[:type] = :directory } + + it "should create destination directory" do + expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag) + end + end end end end @@ -87,40 +92,121 @@ describe "VagrantPlugins::GuestLinux::Cap::FileSystem" do let(:cap) { caps.get(:decompress_zip) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } - let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_zip(machine, comp, dest, opts) } - it "should create temporary directory for extraction" do - expect(cap).to receive(:create_tmp_path) + [false, true].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + let(:opts) { {sudo: sudo_flag} } + + it "should create temporary directory for extraction" do + expect(cap).to receive(:create_tmp_path) + end + + it "should extract file with zip" do + expect(comm).to receive(:execute).with(/zip/, sudo: sudo_flag) + end + + it "should extract file to temporary directory" do + expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag) + end + + it "should remove compressed file from guest" do + expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag) + end + + it "should remove extraction directory from guest" do + expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag) + end + + it "should create parent directories for destination" do + expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag) + end + + context "when type is directory" do + before { opts[:type] = :directory } + + it "should create destination directory" do + expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag) + end + end + end end + end - it "should extract file with zip" do - expect(comm).to receive(:execute).with(/zip/) - end + describe ".create_directories" do + let(:cap) { caps.get(:create_directories) } + let(:dirs) { %w(dir1 dir2) } - it "should extract file to temporary directory" do - expect(comm).to receive(:execute).with(/TMP_DIR/) - end + before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } - it "should remove compressed file from guest" do - expect(comm).to receive(:execute).with(/rm .*#{comp}/) - end + [false, true].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + let(:opts) { {sudo: sudo_flag} } - it "should remove extraction directory from guest" do - expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) - end + after { expect(cap.create_directories(machine, dirs, opts)).to eql(dirs) } - it "should create parent directories for destination" do - expect(comm).to receive(:execute).with(/mkdir -p .*to'/) - end + context "passes directories to be create" do + let(:temp_file) do + double("temp_file").tap do |temp_file| + allow(temp_file).to receive(:binmode) + allow(temp_file).to receive(:close) + allow(temp_file).to receive(:path).and_return("temp_path") + allow(temp_file).to receive(:unlink) + end + end + let(:exec_block) do + Proc.new do |arg, &proc| + lines = arg.split("\n") + expect(lines[lines.length - 2]).to match(/TMP_DIR/) + dirs.each do |dir| + proc.call :stdout, "mkdir: created directory '#{dir}'\n" + end + end + end - context "when type is directory" do - before { opts[:type] = :directory } + before do + allow(Tempfile).to receive(:new).and_return(temp_file) + allow(temp_file).to receive(:write) + allow(temp_file).to receive(:close) + allow(comm).to receive(:upload) + allow(comm).to receive(:execute, &exec_block) + end - it "should create destination directory" do - expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) + it "creates temporary file on guest" do + expect(cap).to receive(:create_tmp_path) + end + + it "creates a temporary file to write dir list" do + expect(Tempfile).to receive(:new).and_return(temp_file) + end + + it "writes dir list to a local temporary file" do + expect(temp_file).to receive(:write).with(dirs.join("\n") + "\n") + end + + it "uploads the local temporary file with dir list to guest" do + expect(comm).to receive(:upload).with("temp_path", "TMP_DIR") + end + + it "executes bash script to create directories on guest" do + expect(comm).to receive(:execute, &exec_block).with(/bash -c .*/, sudo: sudo_flag) + end + end + + context "passes empty dir list" do + let(:dirs) { [] } + + after { expect(cap.create_directories(machine, dirs, opts)).to eql([]) } + + it "does nothing" do + expect(cap).to receive(:create_tmp_path).never + expect(Tempfile).to receive(:new).never + expect(comm).to receive(:upload).never + expect(comm).to receive(:execute).never + end + end end end end diff --git a/test/unit/plugins/guests/linux/cap/hyperv_daemons_test.rb b/test/unit/plugins/guests/linux/cap/hyperv_daemons_test.rb new file mode 100644 index 000000000..737d2f0f4 --- /dev/null +++ b/test/unit/plugins/guests/linux/cap/hyperv_daemons_test.rb @@ -0,0 +1,109 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestLinux::Cap::HypervDaemons" do + let(:caps) do + VagrantPlugins::GuestLinux::Plugin + .components + .guest_capabilities[:linux] + end + + let(:hyperv_services) do + %w[kvp vss fcopy] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".hyperv_daemons_running" do + let(:cap) { caps.get(:hyperv_daemons_running) } + + before do + comm.stub_command("which apt-get", exit_code: 0) + hyperv_services.each do |service| + name = ['hv', service, 'daemon'].join('-') + comm.stub_command("systemctl -q is-active #{name}", exit_code: 0) + end + expect(cap.hyperv_daemons_running(machine)).to be_truthy + end + + it "checks whether guest OS is apt based" do + expect(comm.received_commands[0]).to match(/which apt-get/) + end + + it "checks daemon service status by systemctl" do + hyperv_services.each_with_index do |service, idx| + name = ['hv', service, 'daemon'].join('-') + expect(comm.received_commands[idx + 1]).to match(/systemctl -q is-active #{name}/) + end + end + end + + describe ".hyperv_daemons_installed" do + let(:cap) { caps.get(:hyperv_daemons_installed) } + + before do + hyperv_services.each do |service| + comm.stub_command("which #{['hv', service, 'daemon'].join('_')}", exit_code: 0) + end + + expect(cap.hyperv_daemons_installed(machine)).to be_truthy + end + + it "checks whether hyperv daemons exist on the path" do + hyperv_services.each_with_index do |service, idx| + name = ['hv', service, 'daemon'].join('_') + expect(comm.received_commands[idx]).to match(/which #{name}/) + end + end + end + + describe ".hyperv_daemons_activate" do + let(:cap) { caps.get(:hyperv_daemons_activate) } + let(:hyperv_service_names) { [] } + + before do + comm.stub_command("which apt-get", exit_code: apt_get? ? 0 : 1) + hyperv_services.each do |service| + name = ['hv', service, 'daemon'].join(service_separator) + comm.stub_command("systemctl enable #{name}", exit_code: 0) + comm.stub_command("systemctl restart #{name}", exit_code: 0) + comm.stub_command("systemctl -q is-active #{name}", exit_code: 0) + hyperv_service_names << name + end + expect(cap.hyperv_daemons_activate(machine)).to be_truthy + end + + { debian: { apt_get?: true, + service_separator: "-" }, + linux: { apt_get?: false, + service_separator: "_" } }.map do |guest_type, guest_opts| + + context guest_type do + let(:apt_get?) { guest_opts[:apt_get?] } + let(:service_separator) { guest_opts[:service_separator] } + + it "checks whether guest OS is apt based" do + expect(comm.received_commands[0]).to match(/which apt-get/) + end + + it "checks whether hyperv daemons are activated on Debian/Ubuntu" do + pos = 1 + hyperv_service_names.each do |name| + expect(comm.received_commands[pos]).to match(/systemctl enable #{name}/) + expect(comm.received_commands[pos + 1]).to match(/systemctl restart #{name}/) + expect(comm.received_commands[pos + 2]).to match(/systemctl -q is-active #{name}/) + pos += 3 + end + end + end + end + end +end diff --git a/test/unit/plugins/guests/redhat/cap/hyperv_daemons_test.rb b/test/unit/plugins/guests/redhat/cap/hyperv_daemons_test.rb new file mode 100644 index 000000000..37b0ff953 --- /dev/null +++ b/test/unit/plugins/guests/redhat/cap/hyperv_daemons_test.rb @@ -0,0 +1,47 @@ +require_relative "../../../../base" + +describe "VagrantPlugins::GuestRedHat::Cap::HypervDaemons" do + let(:caps) do + VagrantPlugins::GuestRedHat::Plugin + .components + .guest_capabilities[:redhat] + end + + let(:machine) { double("machine") } + let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) } + + before do + allow(machine).to receive(:communicate).and_return(comm) + end + + after do + comm.verify_expectations! + end + + describe ".hyperv_daemons_installed" do + let(:cap) { caps.get(:hyperv_daemons_installed) } + + it "checks whether hyperv-daemons package is installed" do + cap.hyperv_daemons_installed(machine) + expect(comm.received_commands[0]).to match(/rpm -q hyperv-daemons/) + end + end + + describe ".hyperv_daemons_install" do + let(:cap) { caps.get(:hyperv_daemons_install) } + let(:cmd) do + <<-EOH.gsub(/^ {12}/, "") + if command -v dnf; then + dnf -y install hyperv-daemons + else + yum -y install hyperv-daemons + fi + EOH + end + + it "install hyperv-daemons package" do + cap.hyperv_daemons_install(machine) + expect(comm.received_commands[0]).to match(/if command -v dnf; then\n dnf -y install hyperv-daemons\nelse\n yum -y install hyperv-daemons\nfi\n/) + end + end +end diff --git a/test/unit/plugins/guests/solaris/cap/file_system_test.rb b/test/unit/plugins/guests/solaris/cap/file_system_test.rb index 6f7288aad..58bb4c7d9 100644 --- a/test/unit/plugins/guests/solaris/cap/file_system_test.rb +++ b/test/unit/plugins/guests/solaris/cap/file_system_test.rb @@ -45,40 +45,45 @@ describe "VagrantPlugins::GuestSolaris::Cap::FileSystem" do let(:cap) { caps.get(:decompress_tgz) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } - let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_tgz(machine, comp, dest, opts) } - it "should create temporary directory for extraction" do - expect(cap).to receive(:create_tmp_path) - end + [false, true].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + let(:opts) { {sudo: sudo_flag} } - it "should extract file with tar" do - expect(comm).to receive(:execute).with(/tar/) - end + it "should create temporary directory for extraction" do + expect(cap).to receive(:create_tmp_path) + end - it "should extract file to temporary directory" do - expect(comm).to receive(:execute).with(/TMP_DIR/) - end + it "should extract file with tar" do + expect(comm).to receive(:execute).with(/tar/, sudo: sudo_flag) + end - it "should remove compressed file from guest" do - expect(comm).to receive(:execute).with(/rm .*#{comp}/) - end + it "should extract file to temporary directory" do + expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag) + end - it "should remove extraction directory from guest" do - expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) - end + it "should remove compressed file from guest" do + expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag) + end - it "should create parent directories for destination" do - expect(comm).to receive(:execute).with(/mkdir -p .*to'/) - end + it "should remove extraction directory from guest" do + expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag) + end - context "when type is directory" do - before { opts[:type] = :directory } + it "should create parent directories for destination" do + expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag) + end - it "should create destination directory" do - expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) + context "when type is directory" do + before { opts[:type] = :directory } + + it "should create destination directory" do + expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag) + end + end end end end @@ -87,40 +92,45 @@ describe "VagrantPlugins::GuestSolaris::Cap::FileSystem" do let(:cap) { caps.get(:decompress_zip) } let(:comp) { "compressed_file" } let(:dest) { "path/to/destination" } - let(:opts) { {} } before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } after{ cap.decompress_zip(machine, comp, dest, opts) } - it "should create temporary directory for extraction" do - expect(cap).to receive(:create_tmp_path) - end + [false, true].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + let(:opts) { {sudo: sudo_flag} } - it "should extract file with zip" do - expect(comm).to receive(:execute).with(/zip/) - end + it "should create temporary directory for extraction" do + expect(cap).to receive(:create_tmp_path) + end - it "should extract file to temporary directory" do - expect(comm).to receive(:execute).with(/TMP_DIR/) - end + it "should extract file with zip" do + expect(comm).to receive(:execute).with(/zip/, sudo: sudo_flag) + end - it "should remove compressed file from guest" do - expect(comm).to receive(:execute).with(/rm .*#{comp}/) - end + it "should extract file to temporary directory" do + expect(comm).to receive(:execute).with(/TMP_DIR/, sudo: sudo_flag) + end - it "should remove extraction directory from guest" do - expect(comm).to receive(:execute).with(/rm .*TMP_DIR/) - end + it "should remove compressed file from guest" do + expect(comm).to receive(:execute).with(/rm .*#{comp}/, sudo: sudo_flag) + end - it "should create parent directories for destination" do - expect(comm).to receive(:execute).with(/mkdir -p .*to'/) - end + it "should remove extraction directory from guest" do + expect(comm).to receive(:execute).with(/rm .*TMP_DIR/, sudo: sudo_flag) + end - context "when type is directory" do - before { opts[:type] = :directory } + it "should create parent directories for destination" do + expect(comm).to receive(:execute).with(/mkdir -p .*to'/, sudo: sudo_flag) + end - it "should create destination directory" do - expect(comm).to receive(:execute).with(/mkdir -p .*destination'/) + context "when type is directory" do + before { opts[:type] = :directory } + + it "should create destination directory" do + expect(comm).to receive(:execute).with(/mkdir -p .*destination'/, sudo: sudo_flag) + end + end end end end diff --git a/test/unit/plugins/guests/windows/cap/file_system_test.rb b/test/unit/plugins/guests/windows/cap/file_system_test.rb index ca093399e..a92a6e1c9 100644 --- a/test/unit/plugins/guests/windows/cap/file_system_test.rb +++ b/test/unit/plugins/guests/windows/cap/file_system_test.rb @@ -1,3 +1,4 @@ +require 'json' require_relative "../../../../base" describe "VagrantPlugins::GuestWindows::Cap::FileSystem" do @@ -82,4 +83,88 @@ describe "VagrantPlugins::GuestWindows::Cap::FileSystem" do end end end + + describe ".create_directories" do + let(:cap) { caps.get(:create_directories) } + let(:dirs) { %w(dir1 dir2) } + + before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") } + after { expect(cap.create_directories(machine, dirs)).to eql(dirs) } + + context "passes directories to be create" do + let(:temp_file) do + double("temp_file").tap do |temp_file| + allow(temp_file).to receive(:close) + allow(temp_file).to receive(:path).and_return("temp_path") + allow(temp_file).to receive(:unlink) + end + end + let(:sudo_block) do + Proc.new do |arg, &proc| + lines = arg.split("\n") + expect(lines[0]).to match(/TMP_DIR/) + dirs.each do |dir| + proc.call :stdout, { FullName: dir }.to_json + end + end + end + let(:cmd) do + <<-EOH.gsub(/^ {6}/, "") + $files = Get-Content TMP_DIR + foreach ($file in $files) { + if (-Not (Test-Path($file))) { + ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName) + } else { + if (-Not ((Get-Item $file) -is [System.IO.DirectoryInfo])) { + # Remove the file + Remove-Item -Path $file -Force + ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName) + } + } + } + EOH + end + + before do + allow(Tempfile).to receive(:new).and_return(temp_file) + allow(temp_file).to receive(:write) + allow(temp_file).to receive(:close) + allow(comm).to receive(:upload) + allow(comm).to receive(:execute, &sudo_block) + end + + it "creates temporary file on guest" do + expect(cap).to receive(:create_tmp_path) + end + + it "creates a temporary file to write dir list" do + expect(Tempfile).to receive(:new).and_return(temp_file) + end + + it "writes dir list to a local temporary file" do + expect(temp_file).to receive(:write).with(dirs.join("\n") + "\n") + end + + it "uploads the local temporary file with dir list to guest" do + expect(comm).to receive(:upload).with("temp_path", "TMP_DIR") + end + + it "executes bash script to create directories on guest" do + expect(comm).to receive(:execute, &sudo_block).with(cmd, shell: :powershell) + end + end + + context "passes empty dir list" do + let(:dirs) { [] } + + after { expect(cap.create_directories(machine, dirs)).to eql([]) } + + it "does nothing" do + expect(cap).to receive(:create_tmp_path).never + expect(Tempfile).to receive(:new).never + expect(comm).to receive(:upload).never + expect(comm).to receive(:execute).never + end + end + end end diff --git a/test/unit/plugins/guests/windows/cap/hyperv_daemons_test.rb b/test/unit/plugins/guests/windows/cap/hyperv_daemons_test.rb new file mode 100644 index 000000000..5b8bc3543 --- /dev/null +++ b/test/unit/plugins/guests/windows/cap/hyperv_daemons_test.rb @@ -0,0 +1,307 @@ +require 'json' + +require_relative "../../../../base" + +require Vagrant.source_root.join("plugins/guests/windows/cap/hyperv_daemons") + +describe VagrantPlugins::GuestWindows::Cap::HypervDaemons do + HYPERV_DAEMON_SERVICES = %i[kvp vss fcopy] + HYPERV_DAEMON_SERVICE_NAMES = {kvp: "vmickvpexchange", vss: "vmicvss", fcopy: "vmicguestinterface" } + + STOPPED = 1 + RUNNING = 4 + + MANUAL_MODE = 3 + DISABLED_MODE = 4 + + include_context "unit" + + let(:machine) do + double("machine").tap do |machine| + allow(machine).to receive(:communicate).and_return(comm) + end + end + let(:comm) { double("comm") } + + def name_for(service) + HYPERV_DAEMON_SERVICE_NAMES[service] + end + + def service_status(name, running: true, disabled: false) + { "Name" => name, + "Status" => running ? RUNNING : STOPPED, + "StartType" => disabled ? DISABLED_MODE : MANUAL_MODE } + end + + context "test declared methods" do + subject { described_class } + + describe "#hyperv_daemon_running" do + HYPERV_DAEMON_SERVICES.each do |service| + context "daemon #{service}" do + let(:service) { service } + let(:service_name) { name_for(service) } + + it "checks daemon is running" do + expect(subject).to receive(:service_info). + with(comm, service_name).and_return(service_status(service_name, running: true)) + expect(subject.hyperv_daemon_running(machine, service)).to be_truthy + end + + it "checks daemon is not running" do + expect(subject).to receive(:service_info). + with(comm, service_name).and_return(service_status(service_name, running: false)) + expect(subject.hyperv_daemon_running(machine, service)).to be_falsy + end + end + end + end + + describe "#hyperv_daemons_running" do + it "checks hyperv daemons are running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_running).with(machine, service).and_return(true) + end + expect(subject.hyperv_daemons_running(machine)).to be_truthy + end + + it "checks hyperv daemons are not running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_running).with(machine, service).and_return(false) + end + expect(subject.hyperv_daemons_running(machine)).to be_falsy + end + end + + describe "#hyperv_daemon_installed" do + HYPERV_DAEMON_SERVICES.each do |service| + context "daemon #{service}" do + let(:service) { service } + + before { expect(subject.hyperv_daemon_installed(subject, service)).to be_truthy } + + it "does not call communicate#execute" do + expect(comm).to receive(:execute).never + end + end + end + end + + describe "#hyperv_daemons_installed" do + it "checks hyperv daemons are running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_installed).with(machine, service).and_return(true) + end + expect(subject.hyperv_daemons_installed(machine)).to be_truthy + expect(comm).to receive(:execute).never + end + + it "checks hyperv daemons are not running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_installed).with(machine, service).and_return(false) + end + expect(subject.hyperv_daemons_installed(machine)).to be_falsy + expect(comm).to receive(:execute).never + end + end + + describe "#hyperv_daemon_activate" do + HYPERV_DAEMON_SERVICES.each do |service| + context "daemon #{service}" do + let(:service) { service } + let(:service_name) { name_for(service) } + let(:service_disabled_status) { service_status(service_name, disabled: true, running: false) } + let(:service_stopped_status) { service_status(service_name, running: false) } + let(:service_running_status) { service_status(service_name) } + + context "activate succeeds" do + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_truthy } + + it "enables the service when service disabled" do + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_disabled_status) + expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_running_status) + end + + it "only restarts the service when service enabled" do + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_running_status) + expect(subject).to receive(:enable_service).never + expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_running_status) + end + end + + context "activate fails" do + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_falsy } + + it "enables the service when service disabled" do + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_disabled_status) + expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_stopped_status) + end + + it "does not restart service when failed to enable it" do + expect(subject).to receive(:service_info). + with(comm, service_name).and_return(service_disabled_status) + expect(subject).to receive(:enable_service).with(comm, service_name).and_return(false) + expect(subject).to receive(:restart_service).never + end + end + end + end + end + + describe "#hyperv_daemons_activate" do + it "activates hyperv daemons" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_activate).with(machine, service).and_return(true) + end + expect(subject.hyperv_daemons_activate(machine)).to be_truthy + end + + it "fails to activate hyperv daemons" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_activate).with(machine, service).and_return(false) + end + expect(subject.hyperv_daemons_activate(machine)).to be_falsy + end + end + + describe "#hyperv_daemon_activate" do + HYPERV_DAEMON_SERVICES.each do |service| + context "daemon #{service}" do + let(:service) { service } + let(:service_name) { name_for(service) } + let(:service_disabled_status) { service_status(service_name, disabled: true, running: false) } + let(:service_stopped_status) { service_status(service_name, running: false) } + let(:service_running_status) { service_status(service_name) } + + context "activate succeeds" do + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_truthy } + + it "enables the service when service disabled" do + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_disabled_status) + expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_running_status) + end + + it "only restarts the service when service enabled" do + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_running_status) + expect(subject).to receive(:enable_service).never + expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_running_status) + end + end + + context "activate fails" do + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_falsy } + + it "enables the service when service disabled" do + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_disabled_status) + expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true) + expect(subject).to receive(:service_info). + with(comm, service_name).ordered.and_return(service_stopped_status) + end + + it "does not restart service when failed to enable it" do + expect(subject).to receive(:service_info). + with(comm, service_name).and_return(service_disabled_status) + expect(subject).to receive(:enable_service).with(comm, service_name).and_return(false) + expect(subject).to receive(:restart_service).never + end + end + end + end + end + + describe "#service_info" do + let(:service_name) { name_for(:kvp) } + let(:status) { service_status(service_name) } + + it "executes powershell script" do + cmd = "ConvertTo-Json (Get-Service -Name #{service_name})" + expect(comm).to receive(:execute).with(cmd, shell: :powershell) do |&proc| + proc.call :stdout, status.to_json + end + expect(subject.send(:service_info, comm, service_name)).to eq(status) + end + end + + describe "#restart_service" do + let(:service_name) { name_for(:kvp) } + let(:status) { service_status(service_name) } + + it "executes powershell script" do + cmd = "Restart-Service -Name #{service_name} -Force" + expect(comm).to receive(:execute).with(cmd, shell: :powershell) + expect(subject.send(:restart_service, comm, service_name)).to be_truthy + end + end + + describe "#enable_service" do + let(:service_name) { name_for(:kvp) } + let(:status) { service_status(service_name) } + + it "executes powershell script" do + cmd = "Set-Service -Name #{service_name} -StartupType #{MANUAL_MODE}" + expect(comm).to receive(:execute).with(cmd, shell: :powershell) + expect(subject.send(:enable_service, comm, service_name)).to be_truthy + end + end + end + + context "calls through guest capabilities" do + let(:caps) do + VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows] + end + + describe "#hyperv_daemons_running" do + let(:cap) { caps.get(:hyperv_daemons_running) } + + it "checks hyperv daemons are running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(cap).to receive(:hyperv_daemon_running).with(machine, service).and_return(true) + end + expect(cap.hyperv_daemons_running(machine)).to be_truthy + end + end + + describe "#hyperv_daemons_installed" do + let(:cap) { caps.get(:hyperv_daemons_installed) } + + it "checks hyperv daemons are running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(cap).to receive(:hyperv_daemon_installed).with(machine, service).and_return(true) + end + expect(cap.hyperv_daemons_installed(machine)).to be_truthy + end + end + + describe "#hyperv_daemons_activate" do + let(:cap) { caps.get(:hyperv_daemons_activate) } + + it "activates hyperv daemons" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(cap).to receive(:hyperv_daemon_activate).with(machine, service).and_return(true) + end + expect(cap.hyperv_daemons_activate(machine)).to be_truthy + end + end + end + +end diff --git a/test/unit/plugins/providers/hyperv/command/sync_auto_test.rb b/test/unit/plugins/providers/hyperv/command/sync_auto_test.rb new file mode 100644 index 000000000..9c9c3decb --- /dev/null +++ b/test/unit/plugins/providers/hyperv/command/sync_auto_test.rb @@ -0,0 +1,282 @@ +require_relative "../../../../base" + +require Vagrant.source_root.join("plugins/providers/hyperv/sync_helper") +require Vagrant.source_root.join("plugins/providers/hyperv/command/sync_auto") + +describe VagrantPlugins::HyperV::Command::SyncAuto do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:hostpath_mappings) do + { Windows: { "/vagrant-sandbox": 'C:\Users\brian\code\vagrant-sandbox', + "/Not-The-Same-Path": 'C:\Not\The\Same\Path', + "/relative-dir": 'C:\Users\brian\code\relative-dir', + "/vagrant-sandbox-other-dir": 'C:\Users\brian\code\vagrant-sandbox\other-dir' }, + WSL: { "/vagrant-sandbox": "/mnt/c/Users/brian/code/vagrant-sandbox", + "/Not-The-Same-Path": "/mnt/c/Not/The/Same/Path", + "/relative-dir": "/mnt/c/Users/brian/code/relative-dir", + "/vagrant-sandbox-other-dir": "/mnt/c/Users/brian/code/vagrant-sandbox/other-dir" } } + end + let(:synced_folders_empty) { {} } + let(:synced_folders_dupe) do + { "1234": + { type: "hyperv", + exclude: [".git/"], + guestpath: "/vagrant-sandbox" }, + "5678": + { type: "hyperv", + exclude: [".git/"], + guestpath: "/Not-The-Same-Path" }, + "0912": + { type: "hyperv", + exclude: [".git/"], + guestpath: "/relative-dir"} } + end + + let(:helper_class) { VagrantPlugins::HyperV::SyncHelper } + + let(:paths) { {} } + let(:ssh_info) { { username: "vagrant" }} + + before do + I18n.load_path << Vagrant.source_root.join("templates/locales/providers_hyperv.yml") + I18n.reload! + end + + def machine_stub(name) + double(name).tap do |m| + allow(m).to receive(:id).and_return("foo") + allow(m).to receive(:reload).and_return(nil) + allow(m).to receive(:ssh_info).and_return(ssh_info) + allow(m).to receive(:ui).and_return(iso_env.ui) + allow(m).to receive(:provider).and_return(double("provider")) + allow(m).to receive(:state).and_return(double("state", id: :not_created)) + allow(m).to receive(:env).and_return(iso_env) + allow(m).to receive(:config).and_return(double("config")) + + + allow(m.ui).to receive(:error).and_return(nil) + end + end + + subject do + described_class.new(argv, iso_env).tap + end + + + describe "#execute" do + let (:machine) { machine_stub("m") } + let (:excludes) { { dirs: {}, files: {} } } + + let(:config_synced_folders) do + { "/vagrant": + { type: "hyperv", + exclude: [".git/"], + guestpath: "/vagrant-sandbox" }, + "/vagrant/other-dir": + { type: "hyperv", + exclude: [".git/"], + guestpath: "/vagrant-sandbox-other-dir" }, + "/vagrant/relative-dir": + { type: "hyperv", + exclude: [".git/"], + guestpath: "/relative-dir" } } + end + + before do + allow(subject).to receive(:with_target_vms) { |&block| block.call machine } + allow(machine.ui).to receive(:info) + allow(machine.state).to receive(:id).and_return(:created) + allow(machine.provider).to receive(:capability?).and_return(false) + allow(machine.config).to receive(:vm).and_return(true) + allow(machine.config.vm).to receive(:synced_folders).and_return(config_synced_folders) + allow(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_excludes).and_return(excludes) + allow(helper_class).to receive(:sync_single).and_return(true) + allow(Vagrant::Util::Busy).to receive(:busy).and_return(true) + allow(Listen).to receive(:to).and_return(true) + end + + %i[Windows WSL].each do |host_type| + context "in #{host_type} environment" do + let(:host_type) { host_type } + let(:hostpath_mapping) { hostpath_mappings[host_type] } + let (:cached_folders) do + { hyperv: synced_folders_dupe.dup.tap do |folders| + folders.values.each do |folder| + folder[:hostpath] = hostpath_mapping[folder[:guestpath].to_sym] + end + end } + end + + before do + allow(subject).to receive(:synced_folders). + with(machine, cached: true).and_return(cached_folders) + cached_folders[:hyperv].values.each do |folders| + allow(VagrantPlugins::HyperV::SyncHelper).to receive(:expand_path). + with(folders[:hostpath], machine.env.root_path).and_return(folders[:hostpath]) + end + end + + it "syncs all configured folders" do + expect(helper_class).to receive(:sync_single) + subject.execute + end + + it "watches all configured folders for changes" do + expect(machine.ui).to receive(:info). + with("Doing an initial sync...") + cached_folders[:hyperv].values.each do |folder| + expect(machine.ui).to receive(:info).with("Watching: #{folder[:hostpath]}") + end + subject.execute + end + end + end + end + + subject do + described_class.new(argv, iso_env).tap do |s| + allow(s).to receive(:synced_folders).and_return(synced_folders_empty) + end + end + + describe "#callback" do + it "syncs modified folders to the proper path" do + paths["/foo"] = [ + { machine: machine_stub("m1"), opts: double("opts_m1") }, + { machine: machine_stub("m2"), opts: double("opts_m2") }, + ] + paths["/bar"] = [ + { machine: machine_stub("m3"), opts: double("opts_m3") }, + ] + + paths["/foo"].each do |data| + expect(helper_class).to receive(:sync_single). + with(data[:machine], data[:machine].ssh_info, data[:opts]). + once + end + + m = ["/foo/bar"] + a = [] + r = [] + subject.callback(paths, m, a, r) + end + + it "syncs added folders to the proper path" do + paths["/foo"] = [ + { machine: machine_stub("m1"), opts: double("opts_m1") }, + { machine: machine_stub("m2"), opts: double("opts_m2") }, + ] + paths["/bar"] = [ + { machine: machine_stub("m3"), opts: double("opts_m3") }, + ] + + paths["/foo"].each do |data| + expect(helper_class).to receive(:sync_single). + with(data[:machine], data[:machine].ssh_info, data[:opts]). + once + end + + m = [] + a = ["/foo/bar"] + r = [] + subject.callback(paths, m, a, r) + end + + it "syncs removed folders to the proper path" do + paths["/foo"] = [ + { machine: machine_stub("m1"), opts: double("opts_m1") }, + { machine: machine_stub("m2"), opts: double("opts_m2") }, + ] + paths["/bar"] = [ + { machine: machine_stub("m3"), opts: double("opts_m3") }, + ] + + paths["/foo"].each do |data| + expect(helper_class).to receive(:sync_single). + with(data[:machine], data[:machine].ssh_info, data[:opts]). + once + end + + m = [] + a = [] + r = ["/foo/bar"] + subject.callback(paths, m, a, r) + end + + it "doesn't fail if guest error occurs" do + paths["/foo"] = [ + { machine: machine_stub("m1"), opts: double("opts_m1") }, + { machine: machine_stub("m2"), opts: double("opts_m2") }, + ] + paths["/bar"] = [ + { machine: machine_stub("m3"), opts: double("opts_m3") }, + ] + + paths["/foo"].each do |data| + expect(helper_class).to receive(:sync_single). + with(data[:machine], data[:machine].ssh_info, data[:opts]). + and_raise(Vagrant::Errors::MachineGuestNotReady) + end + + m = [] + a = [] + r = ["/foo/bar"] + expect { subject.callback(paths, m, a, r) }. + to_not raise_error + end + + it "doesn't sync machines with no ID" do + paths["/foo"] = [ + { machine: machine_stub("m1"), opts: double("opts_m1") }, + ] + + paths["/foo"].each do |data| + allow(data[:machine]).to receive(:id).and_return(nil) + expect(helper_class).to_not receive(:sync_single) + end + + m = [] + a = [] + r = ["/foo/bar"] + expect { subject.callback(paths, m, a, r) }. + to_not raise_error + end + + context "on failure" do + let(:machine) { machine_stub("m1") } + let(:opts) { double("opts_m1") } + let(:paths) { {"/foo" => [machine: machine, opts: opts]} } + let(:args) { [paths, ["/foo/bar"], [], []] } + + before do + allow_any_instance_of(Vagrant::Errors::VagrantError). + to receive(:translate_error) + allow(machine.ui).to receive(:error) + end + + context "when sync command fails" do + before do + expect(helper_class).to receive(:sync_single).with(machine, machine.ssh_info, opts). + and_raise(Vagrant::Errors::VagrantError) + end + + it "should notify on error" do + expect(machine.ui).to receive(:error) + subject.callback(*args) + end + + it "should not raise error" do + expect { subject.callback(*args) }.not_to raise_error + end + end + end + end +end diff --git a/test/unit/plugins/providers/hyperv/command/sync_test.rb b/test/unit/plugins/providers/hyperv/command/sync_test.rb new file mode 100644 index 000000000..29cdadea1 --- /dev/null +++ b/test/unit/plugins/providers/hyperv/command/sync_test.rb @@ -0,0 +1,82 @@ +require_relative "../../../../base" + +require Vagrant.source_root.join("plugins/providers/hyperv/sync_helper") +require Vagrant.source_root.join("plugins/providers/hyperv/command/sync") + +describe VagrantPlugins::HyperV::Command::Sync do + include_context "unit" + + let(:argv) { [] } + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:communicator) { double("comm") } + + let(:synced_folders) { {} } + + let(:helper_class) { VagrantPlugins::HyperV::SyncHelper } + + subject do + described_class.new(argv, iso_env).tap do |s| + allow(s).to receive(:synced_folders).and_return(synced_folders) + end + end + + before do + iso_env.machine_names.each do |name| + m = iso_env.machine(name, iso_env.default_provider) + allow(m).to receive(:communicate).and_return(communicator) + end + end + + describe "#execute" do + context "with a single machine" do + let(:ssh_info) {{ + private_key_path: [], + username: "vagrant", + }} + let(:provider) { double("provider") } + let(:capability) { double("capability") } + + let(:machine) { iso_env.machine(iso_env.machine_names[0], iso_env.default_provider) } + + before do + allow(communicator).to receive(:ready?).and_return(true) + allow(machine).to receive(:ssh_info).and_return(ssh_info) + allow(machine).to receive(:provider).and_return(provider) + allow(provider).to receive(:capability).and_return(capability) + + synced_folders[:hyperv] = [ + [:one, { + hostpath: 'C:\vagrant', guestpath: '/vagrant' + }], + [:two, { + hostpath: 'C:\vagrant2', guestpath: '/vagrant2' + }] + ] + end + + it "doesn't sync if communicator isn't ready and exits with 1" do + allow(communicator).to receive(:ready?).and_return(false) + + expect(helper_class).to receive(:sync_single).never + + expect(subject.execute).to eql(1) + end + + it "syncs each folder and exits successfully" do + synced_folders[:hyperv].each do |_, opts| + expect(helper_class).to receive(:sync_single). + with(machine, ssh_info, opts). + ordered + end + + expect(subject.execute).to eql(0) + end + end + end +end diff --git a/test/unit/plugins/providers/hyperv/driver_test.rb b/test/unit/plugins/providers/hyperv/driver_test.rb index 585337600..1570590f0 100644 --- a/test/unit/plugins/providers/hyperv/driver_test.rb +++ b/test/unit/plugins/providers/hyperv/driver_test.rb @@ -1,3 +1,4 @@ +require 'json' require_relative "../../../base" require Vagrant.source_root.join("plugins/providers/hyperv/driver") @@ -117,6 +118,88 @@ describe VagrantPlugins::HyperV::Driver do subject.set_vm_integration_services(CustomKey: true) end end + + describe "#sync_files" do + let(:dirs) { %w[dir1 dir2] } + let(:files) { %w[file1 file2] } + let(:guest_ip) do + {}.tap do |ip| + ip["ip"] = "guest_ip" + end + end + let(:windows_path) { "WIN_PATH" } + let(:windows_temp) { "TEMP_DIR" } + let(:wsl_temp) { "WSL_TEMP" } + let(:file_list) { double("file") } + + before do + allow(subject).to receive(:read_guest_ip).and_return(guest_ip) + allow(Vagrant::Util::Platform).to receive(:windows_temp).and_return(windows_temp) + allow(Vagrant::Util::Subprocess).to receive(:execute). + with("wslpath", "-u", "-a", windows_temp).and_return(double(exit_code: 0, stdout: wsl_temp)) + allow(File).to receive(:open) do |fn, type, &proc| + proc.call file_list + + allow(Vagrant::Util::Platform).to receive(:windows_path). + with(fn, :disable_unc).and_return(windows_path) + allow(FileUtils).to receive(:rm_f).with(fn) + end.and_return(file_list) + allow(file_list).to receive(:write).with(files.to_json) + allow(subject).to receive(:execute).with(:sync_files, + vm_id: vm_id, + guest_ip: guest_ip["ip"], + file_list: windows_path) + end + + after { subject.sync_files vm_id, dirs, files, is_win_guest: false } + + %i[Windows WSL].each do |host_type| + context "in #{host_type} environment" do + let(:is_wsl) { host_type == :WSL } + let(:temp_dir) { is_wsl ? wsl_temp : windows_temp } + + before do + allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(is_wsl) + end + + it "reads guest ip" do + expect(subject).to receive(:read_guest_ip).and_return(guest_ip) + end + + it "gets Windows temporary dir where dir list is written" do + expect(Vagrant::Util::Platform).to receive(:windows_temp).and_return(windows_temp) + end + + if host_type == :WSL + it "converts Windows temporary dir to Unix style for WSL" do + expect(Vagrant::Util::Subprocess).to receive(:execute). + with("wslpath", "-u", "-a", windows_temp).and_return(double(exit_code: 0, stdout: wsl_temp)) + end + end + + it "writes dir list to temporary file" do + expect(File).to receive(:open) do |fn, type, &proc| + expect(fn).to match(/#{temp_dir}\/\.hv_sync_files_.*/) + expect(type).to eq('w') + + proc.call file_list + + expect(Vagrant::Util::Platform).to receive(:windows_path). + with(fn, :disable_unc).and_return(windows_path) + expect(FileUtils).to receive(:rm_f).with(fn) + end.and_return(file_list) + expect(file_list).to receive(:write).with(files.to_json) + end + + it "calls sync files powershell script" do + expect(subject).to receive(:execute).with(:sync_files, + vm_id: vm_id, + guest_ip: guest_ip["ip"], + file_list: windows_path) + end + end + end + end end describe "#execute_powershell" do diff --git a/test/unit/plugins/providers/hyperv/sync_helper_test.rb b/test/unit/plugins/providers/hyperv/sync_helper_test.rb new file mode 100644 index 000000000..bc3b83fb1 --- /dev/null +++ b/test/unit/plugins/providers/hyperv/sync_helper_test.rb @@ -0,0 +1,868 @@ +require 'find' +require 'zip' + +require_relative "../../../base" + +require Vagrant.source_root.join("plugins/providers/hyperv/sync_helper") + +describe VagrantPlugins::HyperV::SyncHelper do + subject { described_class } + + let(:vm_id) { "vm_id" } + let(:guest) { double("guest") } + let(:comm) { double("comm") } + let(:machine) { double("machine", provider: provider, guest: guest, id: vm_id, communicate: comm) } + let(:provider) { double("provider", driver: driver) } + let(:driver) { double("driver") } + let(:separators) { { Windows: '\\', WSL: "/" } } + + def random_name + (0...8).map { ('a'..'z').to_a[rand(26)] }.join + end + + def generate_random_file(files, path, separator, is_directory: true) + prefix = is_directory ? "dir" : "file" + fn = [path, "#{prefix}_#{random_name}"].join(separator) + files << fn + allow(subject).to receive(:directory?).with(fn).and_return(is_directory) + fn + end + + def generate_test_data(paths, separator) + files = [] + excludes = { dirs: [], files: [] } + includes = { dirs: [], files: [] } + paths.map do |dir| + files << dir + excluded = dir.end_with?('.vagrant', '.git') + allow(VagrantPlugins::HyperV::SyncHelper).to receive(:directory?).with(dir).and_return(true) + (0..10).map do + fn = generate_random_file(files, dir, separator, is_directory: false) + if excluded + excludes[:files] << fn + else + includes[:files] << fn + end + end + + sub_dir = generate_random_file(files, dir, separator, is_directory: true) + if excluded + excludes[:dirs] << dir + excludes[:dirs] << sub_dir + else + includes[:dirs] << dir + includes[:dirs] << sub_dir + end + + (0..10).map do + fn = generate_random_file(files, sub_dir, separator, is_directory: false) + if excluded + excludes[:files] << fn + else + includes[:files] << fn + end + end + end + { files: files, + excludes: excludes, + includes: includes } + end + + def convert_path(mapping, path, host_type, guest_type, is_file: true) + win_path = path.gsub "/vagrant", 'C:\vagrant' + win_path.tr! "/", '\\' + + linux_path = path.gsub 'C:\vagrant', "/vagrant" + linux_path.tr! '\\', "/" + + dir_win_path = is_file ? win_path.split("\\")[0..-2].join("\\") : win_path + dir_win_path = dir_win_path[0..-2] if dir_win_path.end_with? '\\', '/' + + dir_linux_path = is_file ? linux_path.split("/")[0..-2].join("/") : linux_path + dir_linux_path = dir_linux_path[0..-2] if dir_linux_path.end_with? '\\', '/' + + guest_path = + if host_type == :WSL + if guest_type == :linux + dir_linux_path + else + dir_win_path + end + else + # windows + if guest_type == :linux + dir_linux_path + else + dir_win_path + end + end + mapping[:hyperv][win_path] = guest_path + mapping[:platform][host_type == :Windows ? win_path : linux_path] = guest_path + end + + describe "#expand_excludes" do + let(:hostpath) { 'vagrant' } + let(:expanded_hostpaths) do + { Windows: 'C:\vagrant', WSL: "/vagrant" } + end + let(:exclude) { [".git/"] } + let(:exclude_dirs) { %w[.vagrant/ .git/] } + + %i[Windows WSL].map do |host_type| + context "in #{host_type} environment" do + let(:host_type) { host_type } + let(:is_windows) { host_type == :Windows } + let(:separator) { separators[host_type] } + let(:expanded_hostpath) { expanded_hostpaths[host_type] } + + before do + allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(false) + allow(subject).to receive(:expand_path).with(hostpath).and_return(expanded_hostpath) + end + + it "expands excludes into file and dir list" do + files = [] + dirs = [] + exclude_dirs.map do |dir| + fullpath = [expanded_hostpath, dir[0..-2]].join(separator) + allow(subject).to receive(:platform_join). + with(expanded_hostpath, dir, is_windows: false).and_return(fullpath) + + ignore_paths = [] + allow(described_class).to receive(:directory?).with(fullpath).and_return(true) + ignore_paths << fullpath + dirs << fullpath + + file = generate_random_file(ignore_paths, fullpath, separator, is_directory: false) + files << file + + subDir = generate_random_file(ignore_paths, fullpath, separator, is_directory: true) + dirs << subDir + + subDirFile = generate_random_file(ignore_paths, subDir, separator, is_directory: false) + files << subDirFile + + allow(Dir).to receive(:glob).with(fullpath) do |arg, &proc| + ignore_paths.each do |path| + proc.call path + end + end + end + excludes = subject.expand_excludes(hostpath, exclude) + expect(excludes[:dirs]).to eq(dirs) + expect(excludes[:files]).to eq(files) + end + end + end + end + + describe "#sync_single" do + let(:hostpath) { 'vagrant' } + let(:expanded_hostpaths) do + { Windows: 'C:\vagrant', WSL: "/vagrant" } + end + let(:guestpaths) do + { windows: 'C:\vagrant', linux: "/vagrant" } + end + let(:remote_guestdirs) do + { windows: 'C:\Windows\tmp', linux: "/tmp" } + end + let(:paths) do + { Windows: %w[C:\vagrant C:\vagrant\.vagrant C:\vagrant\.git C:\vagrant\test], + WSL: %w[/vagrant /vagrant/.vagrant /vagrant/.git /vagrant/test] } + end + let(:exclude) { [".git/"] } + let(:ssh_info) { { username: "vagrant" } } + let(:no_compression) { false } + + %i[windows linux].map do |guest_type| + context "#{guest_type} guest" do + let(:guest_type) { guest_type } + let(:is_win_guest) { guest_type == :windows } + let(:guestpath) { guestpaths[guest_type] } + let(:remote_guestdir) { remote_guestdirs[guest_type] } + + before { allow(guest).to receive(:name).and_return(guest_type) } + + %i[Windows WSL].map do |host_type| + let(:host_type) { host_type } + let(:separator) { separators[host_type] } + let(:input_paths) { paths[host_type] } + let(:expanded_hostpath) { expanded_hostpaths[host_type] } + let(:test_data) { generate_test_data input_paths, separator } + let(:test_includes) { test_data[:includes] } + let(:folder_opts) do + h = { hostpath: hostpath, + guestpath: guestpath, + exclude: exclude } + h = !no_compression ? h : h.dup.merge({no_compression: no_compression}) + h + end + + before do + allow(subject).to receive(:expand_path). + with(hostpath).and_return(expanded_hostpath) + end + + after { subject.sync_single(machine, ssh_info, folder_opts) } + + context "in #{host_type} environment" do + before do + allow(subject).to receive(:find_includes).with(hostpath, exclude).and_return(test_includes) + end + + context "with no compression" do + let(:no_compression) { true } + let(:dir_mappings) do + mappings = { hyperv: {}, platform: {} } + test_includes[:dirs].map do |dir| + convert_path(mappings, dir, host_type, guest_type, is_file: false) + end + mappings + end + let(:files_mappings) do + mappings = { hyperv: {}, platform: {} } + test_includes[:files].map do |file| + convert_path(mappings, file, host_type, guest_type, is_file: true) + end + mappings + end + + before do + allow(subject).to receive(:path_mapping). + with(hostpath, guestpath, test_includes, is_win_guest: is_win_guest). + and_return({dirs: dir_mappings, files: files_mappings}) + allow(subject).to receive(:remove_directory). + with(machine, guestpath, is_win_guest: is_win_guest, sudo: true) + allow(guest).to receive(:capability). + with(:create_directories, dir_mappings[:hyperv].values, sudo: true) + allow(subject).to receive(:hyperv_copy?).with(machine).and_return(true) + allow(driver).to receive(:sync_files). + with(machine.id, dir_mappings[:hyperv], files_mappings[:hyperv], is_win_guest: is_win_guest) + end + + context "copy with Hyper-V daemons" do + it "calls driver#sync_files to sync all files at once" do + expect(driver).to receive(:sync_files). + with(machine.id, dir_mappings[:hyperv], files_mappings[:hyperv], is_win_guest: is_win_guest) + end + end + + context "copy with WinRM" do + let(:stat) { double("stat", symlink?: false)} + + before do + allow(subject).to receive(:hyperv_copy?).with(machine).and_return(false) + allow(subject).to receive(:file_exist?).and_return(true) + allow(subject).to receive(:file_stat).and_return(stat) + allow(comm).to receive(:upload) + end + + it "calls WinRM to upload files" do + files_mappings[:platform].each do |host_path, guest_path| + expect(subject).to receive(:file_exist?).ordered.with(host_path).and_return(true) + expect(subject).to receive(:file_stat).ordered.with(host_path).and_return(stat) + expect(comm).to receive(:upload).ordered.with(host_path, guest_path) + end + end + end + + it "removes destination dir and creates directory structure on guest" do + expect(subject).to receive(:remove_directory). + with(machine, guestpath, is_win_guest: is_win_guest, sudo: true) + expect(guest).to receive(:capability). + with(:create_directories, dir_mappings[:hyperv].values, sudo: true) + end + end + + context "with compression" do + let(:compression_type) { guest_type == :windows ? :zip : :tgz } + let(:remote_guestpath) { [remote_guestdir, "remote_#{compression_type}"].join separator } + let(:archive_name) { [expanded_hostpath, "vagrant_tmp.#{compression_type}"].join separator } + + before do + allow(subject).to receive(:compress_source_zip). + with(expanded_hostpath, test_includes[:files]).and_return(archive_name) + allow(subject).to receive(:compress_source_tgz). + with(expanded_hostpath, test_includes[:files]).and_return(archive_name) + allow(guest).to receive(:capability). + with(:create_tmp_path, extension: ".#{compression_type}").and_return(remote_guestpath) + allow(subject).to receive(:upload_file). + with(machine, archive_name, remote_guestpath, is_win_guest: is_win_guest) + allow(subject).to receive(:remove_directory). + with(machine, guestpath, is_win_guest: is_win_guest, sudo: true) + allow(guest).to receive(:capability). + with("decompress_#{compression_type}".to_sym, remote_guestpath, guestpath, type: :directory, sudo: true) + allow(subject).to receive(:file_exist?).with(archive_name).and_return(true) + allow(FileUtils).to receive(:rm_f).with(archive_name) + end + + it "compresses the host directory to archive" do + expect(subject).to receive("compress_source_#{compression_type}".to_sym). + with(expanded_hostpath, test_includes[:files]).and_return(archive_name) + end + + it "creates temporary path on guest" do + expect(guest).to receive(:capability). + with(:create_tmp_path, extension: ".#{compression_type}").and_return(remote_guestpath) + end + + it "uploads archive file to temporary path on guest" do + allow(subject).to receive(:upload_file). + with(machine, archive_name, remote_guestpath, is_win_guest: is_win_guest) + end + + it "removes destination dir and decompresses archive file at temporary path on guest" do + expect(subject).to receive(:remove_directory). + with(machine, guestpath, is_win_guest: is_win_guest, sudo: true) + expect(guest).to receive(:capability). + with("decompress_#{compression_type}".to_sym, remote_guestpath, guestpath, type: :directory, sudo: true) + end + + it "removes temporary archive file" do + expect(FileUtils).to receive(:rm_f).with(archive_name) + end + end + end + end + end + end + end + + describe "#find_includes" do + let(:hostpath) { 'vagrant' } + let(:expanded_hostpaths) do + { Windows: 'C:\vagrant', WSL: "/vagrant" } + end + let(:exclude) { [".git/"] } + let(:paths) do + { Windows: %w[C:\vagrant C:\vagrant\.vagrant C:\vagrant\.git C:\vagrant\test], + WSL: %w[/vagrant /vagrant/.vagrant /vagrant/.git /vagrant/test] } + end + + %i[windows linux].map do |guest_type| + context "#{guest_type} guest" do + %i[Windows WSL].map do |host_type| + context "in #{host_type} environment" do + let(:host_type) { host_type } + let(:separator) { separators[host_type] } + let(:input_paths) { paths[host_type] } + let(:expanded_hostpath) { expanded_hostpaths[host_type] } + let(:test_data) { generate_test_data input_paths, separator } + let(:test_files) { test_data[:files] } + let(:test_includes) { test_data[:includes] } + let(:test_excludes) { test_data[:excludes] } + + before do + allow(subject).to receive(:expand_path). + with(hostpath).and_return(expanded_hostpath) + allow(subject).to receive(:expand_excludes). + with(hostpath, exclude).and_return(test_excludes) + allow(Find).to receive(:find).with(expanded_hostpath) do |arg, &proc| + test_files.map do |file| + proc.call file + end + end + end + + after do + expect(described_class.send(:find_includes, hostpath, exclude)).to eq(test_includes) + end + + it "expands host path to full path" do + allow(subject).to receive(:expand_path). + with(hostpath).and_return(expanded_hostpath) + end + + it "expands excluded files and directories for exclusion" do + allow(subject).to receive(:expand_excludes). + with(hostpath, exclude).and_return(test_excludes) + end + + it "locates all files in expanded host path" do + expect(Find).to receive(:find).with(expanded_hostpath) + end + end + end + end + end + end + + describe "#path_mapping" do + let(:hostpath) { 'vagrant' } + let(:expanded_hostpaths) do + { Windows: 'C:\vagrant', WSL: "/vagrant" } + end + let(:guestpaths) do + { windows: 'C:\vagrant', linux: "/vagrant" } + end + let(:paths) do + { Windows: %w[C:\vagrant C:\vagrant\.vagrant C:\vagrant\.git C:\vagrant\test], + WSL: %w[/vagrant /vagrant/.vagrant /vagrant/.git /vagrant/test] } + end + + %i[windows linux].map do |guest_type| + context "maps dirs and files for copy on #{guest_type} guest" do + let(:guest_type) { guest_type } + let(:is_win_guest) { guest_type == :windows } + + %i[Windows WSL].map do |host_type| + context "in #{host_type} environment" do + let(:is_windows) { host_type == :Windows } + let(:host_type) { host_type } + let(:separator) { separators[host_type] } + let(:input_paths) { paths[host_type] } + let(:expanded_hostpath) { expanded_hostpaths[host_type] } + let(:expanded_hostpath_windows) { expanded_hostpaths[:Windows] } + let(:guestpath) { guestpaths[guest_type] } + let(:test_data) { generate_test_data input_paths, separator } + let(:includes) { test_data[:includes] } + let(:dir_mappings) do + mappings = { hyperv: {}, platform: {} } + includes[:dirs].map do |dir| + convert_path(mappings, dir, host_type, guest_type, is_file: false) + end + mappings + end + let(:files_mappings) do + mappings = { hyperv: {}, platform: {} } + includes[:files].map do |file| + convert_path(mappings, file, host_type, guest_type, is_file: true) + end + mappings + end + let(:opts) do + shared_folder.dup.tap do |opts| + opts[:guestpath] = guestpath + end + end + + before do + allow(subject).to receive(:expand_path). + with(hostpath).and_return(expanded_hostpath) + allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(!is_windows) + allow(Vagrant::Util::Platform).to receive(:windows_path). + with(expanded_hostpath, :disable_unc).and_return(expanded_hostpath_windows) + end + + after { expect(subject.path_mapping(hostpath, guestpath, includes, is_win_guest: is_win_guest)).to eq({ dirs: dir_mappings, files: files_mappings }) } + + it "expands host path to full path" do + expect(subject).to receive(:expand_path). + with(hostpath).and_return(expanded_hostpath) + end + + it "formats expanded full path to windows path" do + expect(Vagrant::Util::Platform).to receive(:windows_path). + with(expanded_hostpath, :disable_unc).and_return(expanded_hostpath_windows) + end + end + end + end + end + end + + describe "#compress_source_zip" do + let(:windows_temps) { { Windows: 'C:\Windows\tmp', WSL: "/mnt/c/Windows/tmp" } } + let(:paths) do + { Windows: 'C:\vagrant', WSL: "/vagrant" } + end + let(:dir_items) { { Windows: 'C:\vagrant\dir1', WSL: '/vagrant/dir1' } } + let(:source_items) { { Windows: %w(C:\vagrant\file1 C:\vagrant\file2 C:\vagrant\dir1 C:\vagrant\dir1\file2), + WSL: %w(/vagrant/file1 /vagrant/file2 /vagrant/dir1 /vagrant/dir1/file2) } } + + %i[Windows WSL].map do |host_type| + context "in #{host_type} environment" do + let(:is_windows) { host_type == :Windows } + let(:host_type) { host_type } + let(:separator) { separators[host_type] } + let(:windows_temp) { windows_temps[host_type] } + let(:path) { paths[host_type] } + let(:dir_item) { dir_items[host_type] } + let(:platform_source_items) { source_items[host_type] } + let(:zip_path) { [windows_temp, "vagrant_test_tmp.zip"].join separator } + let(:zip_file) { double("zip_file", path: zip_path) } + let(:zip) { double("zip") } + let(:stat) { double("stat", symlink?: false)} + let(:zip_output_stream) { double("zip_output_stream") } + let(:source_file) { double("source_file") } + let(:content) { "ABC" } + + before do + allow(subject).to receive(:format_windows_temp).and_return(windows_temp) + allow(Tempfile).to receive(:create).and_return(zip_file) + allow(zip_file).to receive(:close) + allow(Zip::File).to receive(:open).with(zip_path, Zip::File::CREATE).and_yield(zip) + + allow(subject).to receive(:file_exist?).and_return(true) + allow(subject).to receive(:file_stat).and_return(stat) + allow(subject).to receive(:directory?).and_return(false) + allow(subject).to receive(:directory?).with(dir_item).and_return(true) + + allow(zip).to receive(:get_entry).with(/[\.|dir?]/).and_raise(Errno::ENOENT) + allow(zip).to receive(:mkdir).with(/[\.|dir?]/) + allow(zip).to receive(:get_output_stream).with(/.*file?/).and_yield(zip_output_stream) + allow(File).to receive(:open).with(/.*/, "rb").and_return(source_file) + allow(source_file).to receive(:read).with(2048).and_return(content, nil) + allow(zip_output_stream).to receive(:write).with(content) + end + + after { expect(subject.compress_source_zip(path, platform_source_items)).to eq(zip_path) } + + it "creates a temporary file for writing" do + expect(subject).to receive(:format_windows_temp).and_return(windows_temp) + expect(Tempfile).to receive(:create).and_return(zip_file) + expect(Zip::File).to receive(:open).with(zip_path, Zip::File::CREATE).and_yield(zip) + end + + it "skips directory" do + expect(File).to receive(:open).with(dir_item, "rb").never + end + + it "writes file content to zip archive" do + expect(File).to receive(:open).with(/.*/, "rb").and_return(source_file) + expect(source_file).to receive(:read).with(2048).and_return(content, nil) + expect(zip_output_stream).to receive(:write).with(content) + end + + it "processes all files" do + allow(zip).to receive(:get_output_stream).with(/.*file?/).exactly(platform_source_items.length - 1).times + end + end + end + end + + describe "#compress_source_tgz" do + let(:windows_temps) { { Windows: 'C:\Windows\tmp', WSL: "/mnt/c/Windows/tmp" } } + let(:paths) do + { Windows: 'C:\vagrant', WSL: "/vagrant" } + end + let(:dir_items) { { Windows: 'C:\vagrant\dir1', WSL: '/vagrant/dir1' } } + let(:symlink_items) { { Windows: 'C:\vagrant\file2', WSL: '/vagrant/file2' } } + let(:source_items) { { Windows: %w(C:\vagrant\file1 C:\vagrant\file2 C:\vagrant\dir1 C:\vagrant\dir1\file2), + WSL: %w(/vagrant/file1 /vagrant/file2 /vagrant/dir1 /vagrant/dir1/file2) } } + + %i[Windows WSL].map do |host_type| + context "in #{host_type} environment" do + let(:is_windows) { host_type == :Windows } + let(:host_type) { host_type } + let(:separator) { separators[host_type] } + let(:windows_temp) { windows_temps[host_type] } + let(:path) { paths[host_type] } + let(:dir_item) { dir_items[host_type] } + let(:symlink_item) { symlink_items[host_type] } + let(:platform_source_items) { source_items[host_type] } + let(:tar_path) { [windows_temp, "vagrant_test_tmp.tar"].join separator } + let(:tgz_path) { [windows_temp, "vagrant_test_tmp.tgz"].join separator } + let(:tar_file) { double("tar_file", path: tar_path) } + let(:tgz_file) { double("tgz_file", path: tgz_path) } + let(:tar) { double("tar") } + let(:tgz) { double("tgz") } + let(:file_mode) { 0744 } + let(:stat) { double("stat", symlink?: false, mode: file_mode)} + let(:stat_symlink) { double("stat_symlink", symlink?: true, mode: file_mode)} + let(:tar_io) { double("tar_io") } + let(:source_file) { double("source_file") } + let(:content) { "ABC" } + + before do + allow(subject).to receive(:format_windows_temp).and_return(windows_temp) + allow(Tempfile).to receive(:create).and_return(tar_file, tgz_file) + allow(File).to receive(:open).with(tar_path, "wb+").and_return(tar_file) + allow(File).to receive(:open).with(tgz_path, "wb").and_return(tgz_file) + allow(Gem::Package::TarWriter).to receive(:new).with(tar_file).and_return(tar) + allow(Zlib::GzipWriter).to receive(:new).with(tgz_file).and_return(tgz) + + allow(File).to receive(:delete).with(tar_path) + allow(tar_file).to receive(:close) + allow(tar_file).to receive(:read) + allow(tar_file).to receive(:rewind) + allow(tgz_file).to receive(:close) + + allow(tar).to receive(:mkdir) + allow(tar).to receive(:add_symlink) + allow(tar).to receive(:add_file).and_yield(tar_io) + allow(tar).to receive(:close) + allow(tgz).to receive(:mkdir) + allow(tgz).to receive(:write) + allow(tgz).to receive(:close) + + allow(subject).to receive(:file_exist?).and_return(true) + allow(subject).to receive(:file_stat).and_return(stat) + allow(subject).to receive(:directory?).and_return(false) + allow(subject).to receive(:directory?).with(dir_item).and_return(true) + allow(File).to receive(:open).with(/.*/, "rb").and_yield(source_file) + allow(source_file).to receive(:read).and_return(content, nil) + allow(tar_io).to receive(:write) + end + + after { expect(subject.compress_source_tgz(path, platform_source_items)).to eq(tgz_path) } + + it "creates temporary tar/tgz for writing" do + expect(subject).to receive(:format_windows_temp).and_return(windows_temp) + expect(Tempfile).to receive(:create).and_return(tar_file, tgz_file) + expect(File).to receive(:open).with(tar_path, "wb+").and_return(tar_file) + expect(File).to receive(:open).with(tgz_path, "wb").and_return(tgz_file) + expect(Gem::Package::TarWriter).to receive(:new).with(tar_file).and_return(tar) + expect(Zlib::GzipWriter).to receive(:new).with(tgz_file).and_return(tgz) + end + + it "creates directories in tar" do + expect(tar).to receive(:mkdir).with(dir_item.sub(path, ""), file_mode) + end + + it "writes file content to tar archive" do + expect(File).to receive(:open).with(/.*/, "rb").and_yield(source_file) + expect(source_file).to receive(:read).and_return(content, nil) + expect(tar_io).to receive(:write).with(content) + end + + it "deletes tar file eventually" do + expect(File).to receive(:delete).with(tar_path) + end + + it "processes all files" do + expect(tar).to receive(:add_file).with(/.*file?/, file_mode).exactly(platform_source_items.length - 1).times + end + + it "does not treat symlink as normal file" do + allow(subject).to receive(:file_stat).with(symlink_item).and_return(stat_symlink) + expect(File).to receive(:readlink).with(symlink_item).and_return("/target") + expect(tar).to receive(:add_symlink).with(/.*file?/, "/target", file_mode) + end + end + end + end + + describe "#remove_directory" do + let(:windows_path) { 'C:\Windows\Temp' } + let(:unix_path) { "C:/Windows/Temp" } + + [true, false].each do |sudo_flag| + context "sudo flag: #{sudo_flag}" do + it "calls powershell script to remove directory" do + allow(subject).to receive(:to_windows_path).with(unix_path).and_return(windows_path) + expect(comm).to receive(:execute).with(/.*if \(Test-Path\(\"C:\\Windows\\Temp\"\).*\n.*Remove-Item -Path \"C:\\Windows\\Temp\".*/, shell: :powershell) + subject.remove_directory machine, unix_path, is_win_guest: true, sudo: sudo_flag + end + + it "calls linux command to remove directory forcibly" do + allow(subject).to receive(:to_unix_path).with(windows_path).and_return(unix_path) + expect(comm).to receive(:test).with("test -d '#{unix_path}'").and_return(true) + expect(comm).to receive(:execute).with("rm -rf '#{unix_path}'", sudo: sudo_flag) + subject.remove_directory machine, windows_path, is_win_guest: false, sudo: sudo_flag + end + + it "does not call rm when directory does not exist" do + allow(subject).to receive(:to_unix_path).with(windows_path).and_return(unix_path) + expect(comm).to receive(:test).with("test -d '#{unix_path}'").and_return(false) + expect(comm).to receive(:execute).never + subject.remove_directory machine, windows_path, is_win_guest: false, sudo: sudo_flag + end + end + end + end + + describe "#format_windows_temp" do + let(:windows_temp) { 'C:\Windows\tmp' } + let(:unix_temp) { "/mnt/c/Windows/tmp" } + + before { allow(Vagrant::Util::Platform).to receive(:windows_temp).and_return(windows_temp) } + + it "returns Windows style temporary directory" do + allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(false) + expect(subject.format_windows_temp).to eq(windows_temp) + end + + it "returns Unix style temporary directory in WSL" do + allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(true) + expect(Vagrant::Util::Subprocess).to receive(:execute). + with("wslpath", "-u", "-a", windows_temp).and_return(double(stdout: unix_temp, exit_code: 0)) + expect(subject.format_windows_temp).to eq(unix_temp) + end + end + + describe "#upload_file" do + let(:sources) { { Windows: 'C:\vagrant\file1.zip', WSL: "/vagrant/file1.zip" } } + let(:new_sources) { { Windows: 'C:\Windows\tmp\file2.zip', WSL: "/mnt/c/Windows/tmp/file2.zip" } } + let(:windows_temps) { { Windows: 'C:\Windows\tmp', WSL: "/mnt/c/Windows/tmp" } } + let(:dests) { { windows: 'C:\vagrant\file2.zip', linux: "/vagrant/file2.zip" } } + let(:dest_dirs) { { windows: 'C:\vagrant', linux: "/vagrant" } } + + %i[windows linux].map do |guest_type| + context "#{guest_type} guest" do + let(:guest_type) { guest_type } + let(:dest) { dests[guest_type] } + let(:dest_dir) { dest_dirs[guest_type] } + + %i[Windows WSL].map do |host_type| + context "in #{host_type} environment" do + let(:host_type) { host_type } + let(:is_windows) { host_type == :Windows } + let(:source) { sources[host_type] } + let(:new_source) { new_sources[host_type] } + let(:new_source_windows) { new_sources[:Windows] } + + context "uploads file by Hyper-V daemons when applicable" do + let(:windows_temp) { windows_temps[host_type] } + + before do + allow(subject).to receive(:hyperv_copy?).with(machine).and_return(true) + allow(subject).to receive(:format_windows_temp).and_return(windows_temp) + allow(Vagrant::Util::Platform).to receive(:wsl?).and_return(!is_windows) + allow(Vagrant::Util::Platform).to receive(:windows_path). + with(new_source, :disable_unc).and_return(new_source_windows) + allow(FileUtils).to receive(:rm_f) + allow(FileUtils).to receive(:mv) + allow(subject).to receive(:hyperv_copy) + end + + after { subject.upload_file machine, source, dest, is_win_guest: guest_type == :windows } + + if host_type != :Windows + it "moves the source file to new path with the destination filename" do + expect(FileUtils).to receive(:mv).with(source, new_source) + expect(FileUtils).to receive(:rm_f).with(new_source) + end + end + + it "calls Hyper-V cmdlet to copy file" do + expect(subject).to receive(:hyperv_copy).with(machine, new_source_windows, dest_dir) + end + end + + it "uploads file by WinRM when Hyper-V daemons are not applicable" do + allow(subject).to receive(:hyperv_copy?).with(machine).and_return(false) + expect(comm).to receive(:upload).with(source, dest) + expect(subject).to receive(:hyperv_copy).never + subject.upload_file machine, source, dest, is_win_guest: guest_type == :windows + end + end + end + end + end + end + + describe "#hyperv_copy?" do + before do + allow(guest).to receive(:capability?) + allow(guest).to receive(:capability) + end + + it "does not leverage Hyper-V daemons when guest does not support Hyper-V daemons" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(false) + expect(guest).to receive(:capability).never + end + + it "checks whether Hyper-V daemons are running" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(true) + expect(subject.hyperv_copy?(machine)).to eq(true) + end + end + + describe "#hyperv_copy" do + let(:source) { 'C:\Windows\test' } + let(:dest_dir) { "/vagrant" } + + it "calls Copy-VMFile cmdlet to copy file to guest" do + expect(Vagrant::Util::PowerShell).to receive(:execute_cmd).with(/.*Hyper-V\\Get-VM -Id \"vm_id\"\n.*Hyper-V\\Copy-VMFile -VM \$machine -SourcePath \"C:\\Windows\\test\" -DestinationPath \"#{dest_dir}\".*/) + subject.hyperv_copy(machine, source, dest_dir) + end + end + + describe "#platform_join" do + it "produces Windows-style path" do + expect(subject.platform_join("C:", "vagrant", ".vagrant", "", is_windows: true)).to eq("C:\\vagrant\\.vagrant\\") + end + + it "produces Unix-style path in WSL" do + expect(subject.platform_join("/mnt", "vagrant", ".vagrant", "", is_windows: false)).to eq("/mnt/vagrant/.vagrant/") + end + end + + describe "#platform_path" do + let(:windows_path) { 'C:\Windows\Temp' } + let(:unix_path) { "C:/Windows/Temp" } + + it "returns Windows style path in Windows" do + expect(subject.platform_path(unix_path, is_windows: true)).to eq(windows_path) + end + + it "returns Unix style path in WSL" do + expect(subject.platform_path(windows_path, is_windows: false)).to eq(unix_path) + end + end + + describe "#to_windows_path" do + let(:windows_path) { 'C:\Windows\Temp' } + let(:unix_path) { "C:/Windows/Temp" } + + it "converts path with unix separator to Windows" do + expect(subject.to_windows_path(unix_path)).to eq(windows_path) + end + + it "keeps the original input for Windows path" do + expect(subject.to_windows_path(windows_path)).to eq(windows_path) + end + end + + describe "#to_unix_path" do + let(:windows_path) { '\usr\bin\test' } + let(:unix_path) { "/usr/bin/test" } + + it "converts path with Windows separator to Unix" do + expect(subject.to_unix_path(windows_path)).to eq(unix_path) + end + + it "keeps the original input for Unix path" do + expect(subject.to_unix_path(unix_path)).to eq(unix_path) + end + end + + describe "#trim_head" do + let(:windows_path_no_heading) { 'usr\bin\test' } + let(:unix_path_no_heading) { "usr/bin/test" } + let(:windows_path_with_heading) { '\usr\bin\test' } + let(:unix_path_with_heading) { "/usr/bin/test" } + + it "keeps Windows path with no heading" do + expect(subject.trim_head(windows_path_no_heading)).to eq(windows_path_no_heading) + end + + it "keeps Unix path with no heading" do + expect(subject.trim_head(unix_path_no_heading)).to eq(unix_path_no_heading) + end + + it "removes heading separator from Windows path" do + expect(subject.trim_head(windows_path_with_heading)).to eq(windows_path_no_heading) + end + + it "removes heading separator from Unix path" do + expect(subject.trim_head(unix_path_with_heading)).to eq(unix_path_no_heading) + end + end + + describe "#trim_tail" do + let(:windows_path_no_tailing) { '\usr\bin\test' } + let(:unix_path_no_tailing) { "/usr/bin/test" } + let(:windows_path_with_tailing) { '\usr\bin\test\\' } + let(:unix_path_with_tailing) { "/usr/bin/test/" } + + it "keeps Windows path with no tailing" do + expect(subject.trim_tail(windows_path_no_tailing)).to eq(windows_path_no_tailing) + end + + it "keeps Unix path with no tailing" do + expect(subject.trim_tail(unix_path_no_tailing)).to eq(unix_path_no_tailing) + end + + it "removes tailing separator from Windows path" do + expect(subject.trim_tail(windows_path_with_tailing)).to eq(windows_path_no_tailing) + end + + it "removes tailing separator from Unix path" do + expect(subject.trim_tail(unix_path_with_tailing)).to eq(unix_path_no_tailing) + end + end +end diff --git a/test/unit/plugins/providers/hyperv/synced_folder_test.rb b/test/unit/plugins/providers/hyperv/synced_folder_test.rb new file mode 100644 index 000000000..bf008384b --- /dev/null +++ b/test/unit/plugins/providers/hyperv/synced_folder_test.rb @@ -0,0 +1,136 @@ +require "vagrant" + +require Vagrant.source_root.join("test/unit/base") +require Vagrant.source_root.join("plugins/providers/hyperv/config") +require Vagrant.source_root.join("plugins/providers/hyperv/errors") +require Vagrant.source_root.join("plugins/providers/hyperv/sync_helper") +require Vagrant.source_root.join("plugins/providers/hyperv/synced_folder") + +describe VagrantPlugins::HyperV::SyncedFolder do + include_context "unit" + let(:guest) { double("guest") } + let(:ui) { double("ui") } + let(:ssh_info) { {username: "vagrant"} } + let(:provider) { double("provider") } + let(:machine) do + double("machine").tap do |m| + allow(m).to receive(:provider_config).and_return(VagrantPlugins::HyperV::Config.new) + allow(m).to receive(:provider_name).and_return(:hyperv) + allow(m).to receive(:guest).and_return(guest) + allow(m).to receive(:provider).and_return(provider) + allow(m).to receive(:ssh_info).and_return(ssh_info) + allow(m).to receive(:ui).and_return(ui) + end + end + let(:helper_class) { VagrantPlugins::HyperV::SyncHelper } + + subject { described_class.new } + + before do + I18n.load_path << Vagrant.source_root.join("templates/locales/providers_hyperv.yml") + I18n.reload! + machine.provider_config.finalize! + end + + describe "#usable?" do + it "should be with hyperv provider" do + allow(machine).to receive(:provider_name).and_return(:hyperv) + expect(subject).to be_usable(machine) + end + + it "should not be with another provider" do + allow(machine).to receive(:provider_name).and_return(:vmware_fusion) + expect(subject).not_to be_usable(machine) + end + end + + describe "#share_folders" do + let(:folders) do + { 'folder1' => { hostpath: 'C:\vagrant', guestpath: '/vagrant' }, + 'folder2' => { hostpath: 'C:\vagrant2', guestpath: '/vagrant2' }, + 'ignored' => { hostpath: 'C:\vagrant3' } } + end + + before do + allow(subject).to receive(:configure_hv_daemons).and_return(true) + allow(ui).to receive(:output) + allow(ui).to receive(:info) + allow(ui).to receive(:detail) + allow(helper_class).to receive(:sync_single). + with(machine, ssh_info, + hostpath: 'C:\vagrant', + guestpath: "/vagrant") + allow(helper_class).to receive(:sync_single). + with(machine, ssh_info, + hostpath: 'C:\vagrant2', + guestpath: "/vagrant2") + end + + it "should sync folders" do + subject.send(:enable, machine, folders, {}) + end + end + + describe "#configure_hv_daemons" do + before do + allow(ui).to receive(:info) + allow(ui).to receive(:warn) + end + + it "runs guest which does not support capability :hyperv_daemons_running" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(false) + expect(subject.send(:configure_hv_daemons, machine)).to be_falsy + end + + it "runs guest which has all hyperv daemons running" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(true) + expect(subject.send(:configure_hv_daemons, machine)).to be_truthy + end + + it "runs guest which has hyperv daemons installed but not running" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(false) + allow(guest).to receive(:capability).with(:hyperv_daemons_installed).and_return(true) + allow(guest).to receive(:capability?).with(:hyperv_daemons_activate).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_activate).and_return(true) + expect(subject.send(:configure_hv_daemons, machine)).to be_truthy + end + + it "runs guest which has hyperv daemons installed but cannot activate" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(false) + allow(guest).to receive(:capability).with(:hyperv_daemons_installed).and_return(true) + allow(guest).to receive(:capability?).with(:hyperv_daemons_activate).and_return(false) + expect(subject.send(:configure_hv_daemons, machine)).to be_falsy + end + + it "runs guest which has hyperv daemons installed but activate failed" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(false) + allow(guest).to receive(:capability).with(:hyperv_daemons_installed).and_return(true) + allow(guest).to receive(:capability?).with(:hyperv_daemons_activate).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_activate).and_return(false) + expect(subject.send(:configure_hv_daemons, machine)).to be_falsy + end + + it "runs guest which has no hyperv daemons and unable to install" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(false) + allow(guest).to receive(:capability).with(:hyperv_daemons_installed).and_return(false) + allow(guest).to receive(:capability?).with(:hyperv_daemons_install).and_return(false) + expect(subject.send(:configure_hv_daemons, machine)).to be_falsy + end + + it "runs guest which has hyperv daemons newly installed but failed to activate" do + allow(guest).to receive(:capability?).with(:hyperv_daemons_running).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_running).and_return(false) + allow(guest).to receive(:capability).with(:hyperv_daemons_installed).and_return(false) + allow(guest).to receive(:capability?).with(:hyperv_daemons_install).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_install).and_return(true) + allow(guest).to receive(:capability?).with(:hyperv_daemons_activate).and_return(true) + allow(guest).to receive(:capability).with(:hyperv_daemons_activate).and_return(false) + expect(subject.send(:configure_hv_daemons, machine)).to be_falsy + end + end +end diff --git a/test/unit/vagrant/util/hyperv_daemons_test.rb b/test/unit/vagrant/util/hyperv_daemons_test.rb new file mode 100644 index 000000000..fc09f519b --- /dev/null +++ b/test/unit/vagrant/util/hyperv_daemons_test.rb @@ -0,0 +1,241 @@ +require File.expand_path("../../../base", __FILE__) + +require "vagrant/util/hyperv_daemons" + +describe Vagrant::Util::HypervDaemons do + HYPERV_DAEMON_SERVICES = %i[kvp vss fcopy] + + include_context "unit" + + subject do + klass = described_class + Class.new do + extend klass + end + end + + let(:machine) do + double("machine").tap do |machine| + allow(machine).to receive(:communicate).and_return(comm) + end + end + let(:comm) { double("comm") } + + def name_for(service, separator) + ['hv', service.to_s, 'daemon'].join separator + end + + describe "#hyperv_daemon_running" do + %i[debian linux].each do |guest_type| + context "guest: #{guest_type}" do + let(:guest_type) { guest_type } + let(:is_debian) { guest_type == :debian } + + before do + allow(comm).to receive(:test).with("which apt-get").and_return(is_debian) + end + + HYPERV_DAEMON_SERVICES.each do |service| + context "daemon: #{service}" do + let(:service) { service } + let(:service_name) { name_for(service, is_debian ? '-' : '_') } + + it "checks daemon service is running" do + expect(comm).to receive(:test).with("systemctl -q is-active #{service_name}").and_return(true) + expect(subject.hyperv_daemon_running(machine, service)).to be_truthy + end + + it "checks daemon service is not running" do + expect(comm).to receive(:test).with("systemctl -q is-active #{service_name}").and_return(false) + expect(subject.hyperv_daemon_running(machine, service)).to be_falsy + end + end + end + end + end + end + + describe "#hyperv_daemons_running" do + %i[debian linux].each do |guest_type| + context "guest: #{guest_type}" do + let(:guest_type) { guest_type } + + it "checks hyperv daemons are running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_running).with(machine, service).and_return(true) + end + expect(subject.hyperv_daemons_running(machine)).to be_truthy + end + + it "checks hyperv daemons are not running" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_running).with(machine, service).and_return(false) + end + expect(subject.hyperv_daemons_running(machine)).to be_falsy + end + end + end + end + + describe "#hyperv_daemon_installed" do + %i[debian linux].each do |guest_type| + context "guest: #{guest_type}" do + let(:guest_type) { guest_type } + + HYPERV_DAEMON_SERVICES.each do |service| + context "daemon: #{service}" do + let(:service) { service } + let(:daemon_name) { name_for(service, '_') } + + it "checks daemon is installed" do + allow(comm).to receive(:test).with("which #{daemon_name}").and_return(true) + expect(subject.hyperv_daemon_installed(machine, service)).to be_truthy + end + + it "checks daemon is not installed" do + allow(comm).to receive(:test).with("which #{daemon_name}").and_return(false) + expect(subject.hyperv_daemon_installed(machine, service)).to be_falsy + end + end + end + end + end + end + + describe "#hyperv_daemons_installed" do + %i[debian linux].each do |guest_type| + context "guest: #{guest_type}" do + let(:guest_type) { guest_type } + + it "checks hyperv daemons are installed" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_installed).with(machine, service).and_return(true) + end + expect(subject.hyperv_daemons_installed(machine)).to be_truthy + end + + it "checks hyperv daemons are not installed" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_installed).with(machine, service).and_return(false) + end + expect(subject.hyperv_daemons_installed(machine)).to be_falsy + end + end + end + end + + describe "#hyperv_daemon_activate" do + %i[debian linux].each do |guest_type| + context "guest: #{guest_type}" do + let(:guest_type) { guest_type } + let(:is_debian) { guest_type == :debian } + + before do + allow(comm).to receive(:test).with("which apt-get").and_return(is_debian) + end + + HYPERV_DAEMON_SERVICES.each do |service| + context "daemon: #{service}" do + let(:service) { service } + let(:service_name) { name_for(service, is_debian ? '-' : '_') } + + before do + allow(comm).to receive(:test).with("systemctl enable #{service_name}", sudo: true).and_return(true) + allow(comm).to receive(:test).with("systemctl restart #{service_name}", sudo: true).and_return(true) + allow(comm).to receive(:test).with("systemctl -q is-active #{service_name}").and_return(true) + end + + context "activation succeeds" do + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_truthy } + + it "tests whether enabling service succeeds" do + expect(comm).to receive(:test).with("systemctl enable #{service_name}", sudo: true).and_return(true) + end + + it "tests whether restart service succeeds" do + expect(comm).to receive(:test).with("systemctl restart #{service_name}", sudo: true).and_return(true) + end + + it "checks whether service is active after restart" do + expect(comm).to receive(:test).with("systemctl -q is-active #{service_name}").and_return(true) + end + end + + context "fails to enable" do + before { allow(comm).to receive(:test).with("systemctl enable #{service_name}", sudo: true).and_return(false) } + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_falsy } + + it "tests whether enabling service succeeds" do + expect(comm).to receive(:test).with("systemctl enable #{service_name}", sudo: true).and_return(false) + end + + it "does not try to restart service" do + expect(comm).to receive(:test).with("systemctl restart #{service_name}", sudo: true).never + end + + it "does not check the service status" do + expect(comm).to receive(:test).with("systemctl -q is-active #{service_name}").never + end + end + + context "fails to restart" do + before { allow(comm).to receive(:test).with("systemctl restart #{service_name}", sudo: true).and_return(false) } + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_falsy } + + it "tests whether enabling service succeeds" do + expect(comm).to receive(:test).with("systemctl enable #{service_name}", sudo: true).and_return(true) + end + + it "tests whether restart service succeeds" do + expect(comm).to receive(:test).with("systemctl restart #{service_name}", sudo: true).and_return(false) + end + + it "does not check the service status" do + expect(comm).to receive(:test).with("systemctl -q is-active #{service_name}").never + end + end + + context "restarts the service but still not active" do + before { allow(comm).to receive(:test).with("systemctl -q is-active #{service_name}").and_return(false) } + after { expect(subject.hyperv_daemon_activate(machine, service)).to be_falsy } + + it "tests whether enabling service succeeds" do + expect(comm).to receive(:test).with("systemctl enable #{service_name}", sudo: true).and_return(true) + end + + it "tests whether restart service succeeds" do + expect(comm).to receive(:test).with("systemctl restart #{service_name}", sudo: true).and_return(true) + end + + it "checks whether service is active after restart" do + expect(comm).to receive(:test).with("systemctl -q is-active #{service_name}").and_return(false) + end + end + end + end + end + end + end + + describe "#hyperv_daemons_activate" do + %i[debian linux].each do |guest_type| + context "guest: #{guest_type}" do + let(:guest_type) { guest_type } + + it "activates hyperv daemons" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_activate).with(machine, service).and_return(true) + end + expect(subject.hyperv_daemons_activate(machine)).to be_truthy + end + + it "fails to activate hyperv daemons" do + HYPERV_DAEMON_SERVICES.each do |service| + expect(subject).to receive(:hyperv_daemon_activate).with(machine, service).and_return(false) + end + expect(subject.hyperv_daemons_activate(machine)).to be_falsy + end + end + end + end +end diff --git a/test/unit/vagrant/util/platform_test.rb b/test/unit/vagrant/util/platform_test.rb index 7149a66e3..b61e70599 100644 --- a/test/unit/vagrant/util/platform_test.rb +++ b/test/unit/vagrant/util/platform_test.rb @@ -533,5 +533,15 @@ EOF expect(subject.wsl_drvfs_path?("/home/vagrant/some/path")).to be_falsey end end + + describe ".windows_temp" do + let(:temp_dir) { 'C:\Users\User\AppData\Local\Temp' } + + it "should return windows temporary directory" do + allow(Vagrant::Util::PowerShell).to receive(:execute_cmd). + with("(Get-Item Env:TEMP).Value").and_return(temp_dir) + expect(subject.windows_temp).to eql(temp_dir) + end + end end end