Adds Hyper-V synced folder plugin
This PR adds synced folder plugin for Hyper-V provider. This plugin leverages Hyper-V daemons (hv_fcopy_daemon) to sync files from host in a realiable way for supported guest operation systems over SMB and rsync.
This commit is contained in:
parent
7e0e7ff7e9
commit
38bbe2ba8a
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -651,6 +651,19 @@ module Vagrant
|
|||
@_wsl_windows_appdata_local
|
||||
end
|
||||
|
||||
# Fetch the Windows temp directory
|
||||
#
|
||||
# @return [String, Nil]
|
||||
def windows_temp
|
||||
if !@_windows_temp
|
||||
result = Util::Subprocess.execute("cmd.exe", "/c", "echo %TEMP%")
|
||||
if result.exit_code == 0
|
||||
@_windows_temp = result.stdout.gsub("\"", "").strip
|
||||
end
|
||||
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!
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -73,6 +73,41 @@ module VagrantPlugins
|
|||
cmds.each{ |cmd| comm.execute(cmd) }
|
||||
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)
|
||||
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 = []
|
||||
machine.communicate.sudo("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}'
|
||||
") do |type, data|
|
||||
if type == :stdout && /^.*\'(?<dir>.*)\'/ =~ data
|
||||
created_paths << dir.strip
|
||||
end
|
||||
end
|
||||
created_paths
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -107,10 +107,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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
require 'find'
|
||||
|
||||
require_relative '../helper'
|
||||
|
||||
module VagrantPlugins
|
||||
module HyperV
|
||||
module Cap
|
||||
module SyncFolder
|
||||
def self.sync_folder(machine, data)
|
||||
is_win_guest = machine.guest.name == :windows
|
||||
host_path = VagrantPlugins::HyperV::SyncHelper.expand_path(data[:hostpath])
|
||||
guest_path = data[:guestpath]
|
||||
win_host_path = Vagrant::Util::Platform.format_windows_path(
|
||||
host_path, :disable_unc)
|
||||
win_guest_path = guest_path.tr '/', '\\'
|
||||
|
||||
includes = find_includes(data[:hostpath], data[:exclude])
|
||||
dir_mappings = {}
|
||||
file_mappings = {}
|
||||
platform_guest_path = is_win_guest ? win_guest_path : guest_path
|
||||
{ dirs: dir_mappings,
|
||||
files: file_mappings }.map do |sym, mapping|
|
||||
includes[sym].map do |e|
|
||||
guest_rel = e.gsub(host_path, '')
|
||||
guest_rel = guest_rel[1..-1] if guest_rel.start_with? '\\', '/'
|
||||
guest_rel.tr! '\\', '/'
|
||||
|
||||
# make sure the dir names are Windows-style for them to pass to Hyper-V
|
||||
if guest_rel == ''
|
||||
win_path = win_host_path
|
||||
target = platform_guest_path
|
||||
else
|
||||
win_path = HyperV::SyncHelper.platform_join(win_host_path, guest_rel)
|
||||
target = HyperV::SyncHelper.platform_join(platform_guest_path, guest_rel,
|
||||
is_windows: is_win_guest)
|
||||
end
|
||||
mapping[win_path] = target
|
||||
end
|
||||
end
|
||||
machine.guest.capability(:create_directories, dir_mappings.values)
|
||||
machine.provider.driver.sync_files(machine.id, dir_mappings, file_mappings,
|
||||
is_win_guest: is_win_guest)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def self.find_includes(path, exclude)
|
||||
expanded_path = HyperV::SyncHelper.expand_path(path)
|
||||
excludes = HyperV::SyncHelper.expand_excludes(path, exclude)
|
||||
included_dirs = []
|
||||
included_files = []
|
||||
Find.find(expanded_path) do |e|
|
||||
if VagrantPlugins::HyperV::SyncHelper.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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
require 'optparse'
|
||||
|
||||
require "vagrant/action/builtin/mixin_synced_folders"
|
||||
|
||||
require_relative "../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
|
|
@ -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 '../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
|
|
@ -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 dirs.to_json
|
||||
end
|
||||
win_path = Vagrant::Util::Platform.format_windows_path(
|
||||
fn, :disable_unc)
|
||||
status = execute(:sync_files,
|
||||
vm_id: vm_id,
|
||||
guest_ip: guest_ip,
|
||||
dir_list: win_path)
|
||||
status
|
||||
ensure
|
||||
FileUtils.rm_f(fn)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def execute_powershell(path, options, &block)
|
||||
|
|
|
@ -41,6 +41,18 @@ module VagrantPlugins
|
|||
class SystemAccessRequired < HyperVError
|
||||
error_key(:system_access_required)
|
||||
end
|
||||
|
||||
class DaemonsNotInstalledInGuest < HyperVError
|
||||
error_key(:daemons_not_installed_in_guest)
|
||||
end
|
||||
|
||||
class DaemonsNotEnabledInGuest < HyperVError
|
||||
error_key(:daemons_not_enabled_in_guest)
|
||||
end
|
||||
|
||||
class DaemonsEnableFailedInGuest < HyperVError
|
||||
error_key(:daemons_enable_failed_in_guest)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
require "vagrant/util/platform"
|
||||
|
||||
module VagrantPlugins
|
||||
module HyperV
|
||||
class SyncHelper
|
||||
WINDOWS_SEPARATOR = "\\"
|
||||
UNIX_SEPARATOR = "/"
|
||||
|
||||
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|
|
||||
excluded_path = platform_join expanded_path, exclude,
|
||||
is_windows: !Vagrant::Util::Platform.wsl?
|
||||
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.platform_join(string, *smth, is_windows: true)
|
||||
joined = [string, *smth].join is_windows ? WINDOWS_SEPARATOR : UNIX_SEPARATOR
|
||||
if is_windows
|
||||
joined.tr! UNIX_SEPARATOR, WINDOWS_SEPARATOR
|
||||
else
|
||||
joined.tr! WINDOWS_SEPARATOR, UNIX_SEPARATOR
|
||||
end
|
||||
joined
|
||||
end
|
||||
|
||||
def self.sync_single(machine, ssh_info, opts)
|
||||
opts = opts.dup
|
||||
opts[:owner] ||= ssh_info[:username]
|
||||
opts[:group] ||= ssh_info[:username]
|
||||
machine.provider.capability(:sync_folder, opts)
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,12 +16,22 @@ 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!
|
||||
Config
|
||||
end
|
||||
|
||||
provider_capability("hyperv", "sync_folder") do
|
||||
require_relative "cap/sync_folder"
|
||||
Cap::SyncFolder
|
||||
end
|
||||
|
||||
provider_capability("hyperv", "public_address") do
|
||||
require_relative "cap/public_address"
|
||||
Cap::PublicAddress
|
||||
|
@ -32,6 +42,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!
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
#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]$dir_list,
|
||||
[string]$path_separator
|
||||
)
|
||||
|
||||
function copy-file($machine, $dir_list, $path_separator) {
|
||||
$files = Get-Content $dir_list | ConvertFrom-Json
|
||||
$succeeded = @()
|
||||
$failed = @()
|
||||
foreach ($line in $files.PSObject.Properties) {
|
||||
$sourceDir = $line.Name
|
||||
$destDir = $line.Value
|
||||
Get-ChildItem $sourceDir -File | ForEach-Object -Process {
|
||||
$from = $sourceDir + "\" + $_.Name
|
||||
$to = $destDir
|
||||
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
|
||||
Break
|
||||
}
|
||||
}
|
||||
}
|
||||
[hashtable]$return = @{}
|
||||
$return.succeeded = $succeeded
|
||||
$return.failed = $failed
|
||||
return $return
|
||||
}
|
||||
|
||||
$machine = Hyper-V\Get-VM -Id $vm_id
|
||||
|
||||
$status = copy-file $machine $dir_list $path_separator
|
||||
|
||||
$resultHash = @{
|
||||
message = "OK"
|
||||
status = $status
|
||||
}
|
||||
$result = ConvertTo-Json $resultHash
|
||||
Write-OutputMessage $result
|
|
@ -0,0 +1,87 @@
|
|||
require "fileutils"
|
||||
require "vagrant/util/platform"
|
||||
|
||||
require_relative '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)
|
||||
return 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)
|
||||
raise Errors::DaemonsNotInstalledInGuest unless can_install
|
||||
|
||||
machine.ui.info I18n.t("vagrant_hyperv.daemons.installing")
|
||||
machine.guest.capability(:hyperv_daemons_install)
|
||||
end
|
||||
|
||||
can_activate = machine.guest.capability?(:hyperv_daemons_activate)
|
||||
raise Errors::DaemonsNotEnabledInGuest unless can_activate
|
||||
|
||||
machine.ui.info I18n.t("vagrant_hyperv.daemons.activating")
|
||||
activated = machine.guest.capability(:hyperv_daemons_activate)
|
||||
raise Errors::DaemonsEnableFailedInGuest unless activated
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# This is here so that we can stub it for tests
|
||||
def driver(machine)
|
||||
machine.provider.driver
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,6 +10,41 @@ 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}"
|
||||
daemons:
|
||||
installing: |-
|
||||
Installing Hyper-V daemons...
|
||||
activating: |-
|
||||
Activating Hyper-V daemons...
|
||||
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: |-
|
||||
|
@ -106,3 +141,18 @@ en:
|
|||
problem:
|
||||
|
||||
icacls.exe %{root_dir} /T /Q /grant "NT AUTHORITY\SYSTEM:(IO)(CI)(F)"
|
||||
daemons_not_installed_in_guest: |-
|
||||
Hyper-V daemons are not installed in the guest operation system.
|
||||
Hyper-V shared folder plugin relies on Hyper-V daemons to function
|
||||
properly. For guest operation system which does not support Hyper-V
|
||||
daemons, you may use Rsync or SMB plugin instead.
|
||||
daemons_not_enabled_in_guest: |-
|
||||
Hyper-V daemons are not enabled in the guest operation system.
|
||||
Hyper-V shared folder plugin relies on Hyper-V daemons to function
|
||||
properly. For guest operation system which does not support Hyper-V
|
||||
daemons, you may use Rsync or SMB plugin instead.
|
||||
daemons_enable_failed_in_guest: |-
|
||||
Failed to enable Hyper-V daemons in the guest operation system.
|
||||
Hyper-V shared folder plugin relies on Hyper-V daemons to function
|
||||
properly. For guest operation system which does not support Hyper-V
|
||||
daemons, you may use Rsync or SMB plugin instead.
|
||||
|
|
Loading…
Reference in New Issue