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