diff --git a/plugins/guests/linux/cap/choose_addressable_ip_addr.rb b/plugins/guests/linux/cap/choose_addressable_ip_addr.rb new file mode 100644 index 000000000..7584bd77a --- /dev/null +++ b/plugins/guests/linux/cap/choose_addressable_ip_addr.rb @@ -0,0 +1,20 @@ +module VagrantPlugins + module GuestLinux + module Cap + module ChooseAddressableIPAddr + def self.choose_addressable_ip_addr(machine, possible) + machine.communicate.tap do |comm| + possible.each do |ip| + command = "ping -c1 -w1 -W1 #{ip}" + if comm.test(command) + return ip + end + end + end + + nil + end + end + end + end +end diff --git a/plugins/guests/linux/cap/mount_smb_shared_folder.rb b/plugins/guests/linux/cap/mount_smb_shared_folder.rb new file mode 100644 index 000000000..1c0824468 --- /dev/null +++ b/plugins/guests/linux/cap/mount_smb_shared_folder.rb @@ -0,0 +1,80 @@ +module VagrantPlugins + module GuestLinux + module Cap + class MountSMBSharedFolder + def self.mount_smb_shared_folder(machine, name, guestpath, options) + expanded_guest_path = machine.guest.capability( + :shell_expand_guest_path, guestpath) + + mount_commands = [] + mount_device = "//#{options[:smb_host]}/#{name}" + + if options[:owner].is_a? Integer + mount_uid = options[:owner] + else + mount_uid = "`id -u #{options[:owner]}`" + end + + if options[:group].is_a? Integer + mount_gid = options[:group] + mount_gid_old = options[:group] + else + mount_gid = "`getent group #{options[:group]} | cut -d: -f3`" + mount_gid_old = "`id -g #{options[:group]}`" + end + + options[:mount_options] ||= [] + options[:mount_options] << "sec=ntlm" + options[:mount_options] << "username=#{options[:smb_username]}" + options[:mount_options] << "pass=#{options[:smb_password]}" + + # First mount command uses getent to get the group + mount_options = "-o uid=#{mount_uid},gid=#{mount_gid}" + mount_options += ",#{options[:mount_options].join(",")}" if options[:mount_options] + mount_commands << "mount -t cifs #{mount_options} #{mount_device} #{expanded_guest_path}" + + # Second mount command uses the old style `id -g` + mount_options = "-o uid=#{mount_uid},gid=#{mount_gid_old}" + mount_options += ",#{options[:mount_options].join(",")}" if options[:mount_options] + mount_commands << "mount -t cifs #{mount_options} #{mount_device} #{expanded_guest_path}" + + # Create the guest path if it doesn't exist + machine.communicate.sudo("mkdir -p #{expanded_guest_path}") + + # Attempt to mount the folder. We retry here a few times because + # it can fail early on. + attempts = 0 + while true + success = true + + mount_commands.each do |command| + no_such_device = false + status = machine.communicate.sudo(command, error_check: false) do |type, data| + no_such_device = true if type == :stderr && data =~ /No such device/i + end + + success = status == 0 && !no_such_device + break if success + end + + break if success + + attempts += 1 + if attempts > 10 + raise Vagrant::Errors::LinuxMountFailed, + command: mount_commands.join("\n") + end + + sleep 2 + end + + # Emit an upstart event if we can + if machine.communicate.test("test -x /sbin/initctl") + machine.communicate.sudo( + "/sbin/initctl emit --no-wait vagrant-mounted MOUNTPOINT=#{expanded_guest_path}") + end + end + end + end + end +end diff --git a/plugins/guests/linux/plugin.rb b/plugins/guests/linux/plugin.rb index 9a93ab5b9..45c6e2bce 100644 --- a/plugins/guests/linux/plugin.rb +++ b/plugins/guests/linux/plugin.rb @@ -11,6 +11,11 @@ module VagrantPlugins Guest end + guest_capability("linux", "choose_addressable_ip_addr") do + require_relative "cap/choose_addressable_ip_addr" + Cap::ChooseAddressableIPAddr + end + guest_capability("linux", "halt") do require_relative "cap/halt" Cap::Halt @@ -31,6 +36,11 @@ module VagrantPlugins Cap::MountNFS end + guest_capability("linux", "mount_smb_shared_folder") do + require_relative "cap/mount_smb_shared_folder" + Cap::MountSMBSharedFolder + end + guest_capability("linux", "mount_virtualbox_shared_folder") do require_relative "cap/mount_virtualbox_shared_folder" Cap::MountVirtualBoxSharedFolder diff --git a/plugins/providers/hyperv/action.rb b/plugins/providers/hyperv/action.rb index a36e66e9b..b1cda402f 100644 --- a/plugins/providers/hyperv/action.rb +++ b/plugins/providers/hyperv/action.rb @@ -16,14 +16,9 @@ module VagrantPlugins b2.use MessageNotCreated next end + b2.use action_halt - b2.use Call, WaitForState, :off, 120 do |env2, b3| - if env2[:result] - b3.use action_up - else - env2[:ui].info("Machine did not reload, Check machine's status") - end - end + b2.use action_start end end end @@ -58,7 +53,12 @@ module VagrantPlugins b2.use MessageNotCreated next end - b2.use StopInstance + + b2.use Call, GracefulHalt, :off, :running do |env2, b3| + if !env2[:result] + b3.use StopInstance + end + end end end end @@ -69,6 +69,8 @@ module VagrantPlugins b.use StartInstance b.use WaitForIPAddress b.use WaitForCommunicator, [:running] + b.use SyncedFolders + #b.use ShareFolders #b.use SyncFolders end @@ -79,18 +81,11 @@ module VagrantPlugins b.use HandleBox b.use ConfigValidate b.use Call, IsCreated do |env1, b1| - if env1[:result] - b1.use Call, IsStopped do |env2, b2| - if env2[:result] - b2.use action_start - else - b2.use MessageAlreadyCreated - end - end - else + if !env1[:result] b1.use Import - b1.use action_start end + + b1.use action_start end end end @@ -145,7 +140,6 @@ module VagrantPlugins autoload :ReadGuestIP, action_root.join('read_guest_ip') autoload :ShareFolders, action_root.join('share_folders') autoload :WaitForIPAddress, action_root.join("wait_for_ip_address") - autoload :WaitForState, action_root.join('wait_for_state') end end end diff --git a/plugins/providers/hyperv/action/stop_instance.rb b/plugins/providers/hyperv/action/stop_instance.rb index f73e0cefe..132ea8624 100644 --- a/plugins/providers/hyperv/action/stop_instance.rb +++ b/plugins/providers/hyperv/action/stop_instance.rb @@ -7,7 +7,7 @@ module VagrantPlugins end def call(env) - env[:ui].info("Stopping the machine...")) + env[:ui].info("Stopping the machine...") options = { VmId: env[:machine].id } env[:machine].provider.driver.execute('stop_vm.ps1', options) @app.call(env) diff --git a/plugins/providers/hyperv/action/wait_for_state.rb b/plugins/providers/hyperv/action/wait_for_state.rb deleted file mode 100644 index 6ed873410..000000000 --- a/plugins/providers/hyperv/action/wait_for_state.rb +++ /dev/null @@ -1,40 +0,0 @@ -#------------------------------------------------------------------------- -# Copyright (c) Microsoft Open Technologies, Inc. -# All Rights Reserved. Licensed under the MIT License. -#-------------------------------------------------------------------------- -require "log4r" -require "timeout" -require "debugger" - -module VagrantPlugins - module HyperV - module Action - class WaitForState - def initialize(app, env, state, timeout) - @app = app - @state = state - @timeout = timeout - end - - def call(env) - env[:result] = true - # Wait until the Machine's state is disabled (ie State of Halt) - unless env[:machine].state.id == @state - env[:ui].info("Waiting for machine to #{@state}") - begin - Timeout.timeout(@timeout) do - until env[:machine].state.id == @state - sleep 2 - end - end - rescue Timeout::Error - env[:result] = false # couldn't reach state in time - end - end - @app.call(env) - end - - end - end - end -end diff --git a/plugins/providers/hyperv/scripts/set_smb_share.ps1 b/plugins/providers/hyperv/scripts/set_smb_share.ps1 index 6f64183c8..fd8c4e795 100644 --- a/plugins/providers/hyperv/scripts/set_smb_share.ps1 +++ b/plugins/providers/hyperv/scripts/set_smb_share.ps1 @@ -1,46 +1,30 @@ -#------------------------------------------------------------------------- -# Copyright (c) Microsoft Open Technologies, Inc. -# All Rights Reserved. Licensed under the MIT License. -#-------------------------------------------------------------------------- +Param( + [Parameter(Mandatory=$true)] + [string]$path, + [Parameter(Mandatory=$true)] + [string]$share_name, + [Parameter(Mandatory=$true)] + [string]$host_share_username +) -param ( - [string]$path = $(throw "-path is required."), - [string]$share_name = $(throw "-share_name is required."), - [string]$host_share_username = $(throw "-host_share_username is required") - ) +$ErrorAction = "Stop" -# Include the following modules -$presentDir = Split-Path -parent $PSCommandPath -$modules = @() -$modules += $presentDir + "\utils\write_messages.ps1" -forEach ($module in $modules) { . $module } - -try { - # See all available shares and check alert user for existing / conflicting share name - $shared_folders = net share - $reg = "$share_name(\s+)$path(\s)" - $existing_share = $shared_folders -Match $reg - if ($existing_share) { +# See all available shares and check alert user for +# existing/conflicting share name +$shared_folders = net share +$reg = "$share_name(\s+)$path(\s)" +$existing_share = $shared_folders -Match $reg +if ($existing_share) { # Always clear the existing share name and create a new one net share $share_name /delete /y - } - - $computer_name = $(Get-WmiObject Win32_Computersystem).name - $grant_permission = "$computer_name\$host_share_username,Full" - $result = net share $share_name=$path /unlimited /GRANT:$grant_permission - if ($result -Match "$share_name was shared successfully.") { - $resultHash = @{ - message = "OK" - } - $result = ConvertTo-Json $resultHash - Write-Output-Message $result - } else { - $reg = "^$share_name(\s+)" - $existing_share = $shared_folders -Match $reg - Write-Error-Message "IGNORING Conflicting share name, A share name already exist $existing_share" - } -} catch { - Write-Error-Message $_ - return } +$computer_name = $(Get-WmiObject Win32_Computersystem).name +$grant_permission = "$computer_name\$host_share_username,Full" +$result = net share $share_name=$path /unlimited /GRANT:$grant_permission +if ($result -Match "$share_name was shared successfully.") { + exit 0 +} + +$host.ui.WriteErrorLine("Error: $result") +exit 1 diff --git a/plugins/synced_folders/smb/errors.rb b/plugins/synced_folders/smb/errors.rb new file mode 100644 index 000000000..d1279563a --- /dev/null +++ b/plugins/synced_folders/smb/errors.rb @@ -0,0 +1,22 @@ +module VagrantPlugins + module SyncedFolderSMB + module Errors + # A convenient superclass for all our errors. + class SMBError < Vagrant::Errors::VagrantError + error_namespace("vagrant_sf_smb.errors") + end + + class DefineShareFailed < SMBError + error_key(:define_share_failed) + end + + class NoHostIPAddr < SMBError + error_key(:no_routable_host_addr) + end + + class PowershellError < SMBError + error_key(:powershell_error) + end + end + end +end diff --git a/plugins/synced_folders/smb/plugin.rb b/plugins/synced_folders/smb/plugin.rb new file mode 100644 index 000000000..82b029a0b --- /dev/null +++ b/plugins/synced_folders/smb/plugin.rb @@ -0,0 +1,32 @@ +require "vagrant" + +module VagrantPlugins + module SyncedFolderSMB + autoload :Errors, File.expand_path("../errors", __FILE__) + + # This plugin implements SMB synced folders. + class Plugin < Vagrant.plugin("2") + name "SMB synced folders" + description <<-EOF + The SMB synced folders plugin enables you to use SMB folders on + Windows and share them to guest machines. + EOF + + synced_folder("smb", 5) do + require_relative "synced_folder" + init! + SyncedFolder + end + + protected + + def self.init! + return if defined?(@_init) + I18n.load_path << File.expand_path( + "templates/locales/synced_folder_smb.yml", Vagrant.source_root) + I18n.reload! + @_init = true + end + end + end +end diff --git a/plugins/synced_folders/smb/scripts/host_info.ps1 b/plugins/synced_folders/smb/scripts/host_info.ps1 new file mode 100644 index 000000000..7d283a7f4 --- /dev/null +++ b/plugins/synced_folders/smb/scripts/host_info.ps1 @@ -0,0 +1,8 @@ +$ErrorAction = "Stop" + +$net = Get-WmiObject -class win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"' +$result = @{ + ip_addresses = $net.ipaddress +} + +Write-Output $(ConvertTo-Json $result) diff --git a/plugins/synced_folders/smb/scripts/set_share.ps1 b/plugins/synced_folders/smb/scripts/set_share.ps1 new file mode 100644 index 000000000..ea24995fd --- /dev/null +++ b/plugins/synced_folders/smb/scripts/set_share.ps1 @@ -0,0 +1,34 @@ +Param( + [Parameter(Mandatory=$true)] + [string]$path, + [Parameter(Mandatory=$true)] + [string]$share_name, + [string]$host_share_username = $null +) + +$ErrorAction = "Stop" + +# See all available shares and check alert user for existing/conflicting +# share names. +$path_regexp = [System.Text.RegularExpressions.Regex]::Escape($path) +$name_regexp = [System.Text.RegularExpressions.Regex]::Escape($share_name) +$reg = "(?m)$name_regexp\s+$path_regexp\s" +$existing_share = $($(net share) -join "`n") -Match $reg +if ($existing_share) { + # Always clear the existing share name and create a new one + net share $share_name /delete /y +} + +$grant = "Everyone,Full" +if (![string]::IsNullOrEmpty($host_share_username)) { + $computer_name = $(Get-WmiObject Win32_Computersystem).name + $grant = "$computer_name\$host_share_username,Full" +} + +$result = net share $share_name=$path /unlimited /GRANT:$grant +if ($result -Match "$share_name was shared successfully.") { + exit 0 +} + +$host.ui.WriteErrorLine("Error: $result") +exit 1 diff --git a/plugins/synced_folders/smb/synced_folder.rb b/plugins/synced_folders/smb/synced_folder.rb new file mode 100644 index 000000000..b5817038c --- /dev/null +++ b/plugins/synced_folders/smb/synced_folder.rb @@ -0,0 +1,138 @@ +require "json" + +require "log4r" + +require "vagrant/util/platform" +require "vagrant/util/powershell" + +module VagrantPlugins + module SyncedFolderSMB + class SyncedFolder < Vagrant.plugin("2", :synced_folder) + def initialize(*args) + super + + @logger = Log4r::Logger.new("vagrant::synced_folders::smb") + end + + def usable?(machine, raise_error=false) + if !Vagrant::Util::Platform.windows? + # TODO: raise error if specified + return false + end + + true + end + + def prepare(machine, folders, opts) + script_path = File.expand_path("../scripts/set_share.ps1", __FILE__) + + folders.each do |id, data| + hostpath = data[:hostpath] + + data[:smb_id] ||= "#{machine.id}-#{id.gsub("/", "-")}" + + args = [] + args << "-path" << hostpath.gsub("/", "\\") + args << "-share_name" << data[:smb_id] + #args << "-host_share_username" << "mitchellh" + + r = Vagrant::Util::PowerShell.execute(script_path, *args) + if r.exit_code != 0 + raise Errors::DefineShareFailed, + host: hostpath.to_s, + stderr: r.stderr, + stdout: r.stdout + end + end + end + + def enable(machine, folders, nfsopts) + machine.ui.output(I18n.t("vagrant_sf_smb.mounting")) + + # Make sure that this machine knows this dance + if !machine.guest.capability?(:mount_smb_shared_folder) + raise Vagrant::Errors::GuestCapabilityNotFound, + cap: "mount_smb_shared_folder", + guest: machine.guest.name.to_s + end + + # Detect the host IP for this guest if one wasn't specified + # for every folder. + host_ip = nil + need_host_ip = false + folders.each do |id, data| + if !data[:smb_host] + need_host_ip = true + break + end + end + + if need_host_ip + candidate_ips = load_host_ips + @logger.debug("Potential host IPs: #{candidate_ips.inspect}") + host_ip = machine.guest.capability( + :choose_addressable_ip_addr, candidate_ips) + if !host_ip + raise Errors::NoHostIPAddr + end + end + + # If we need auth information, then ask the user + username = nil + password = nil + need_auth = false + folders.each do |id, data| + if !data[:smb_username] || !data[:smb_password] + need_auth = true + break + end + end + + if need_auth + machine.ui.detail(I18n.t("vagrant_sf_smb.warning_password") + "\n ") + username = machine.ui.ask("Username: ") + password = machine.ui.ask("Password (will be hidden): ", echo: false) + end + + # This is used for defaulting the owner/group + ssh_info = machine.ssh_info + + folders.each do |id, data| + data = data.dup + data[:smb_host] ||= host_ip + data[:smb_username] ||= username + data[:smb_password] ||= password + + # Default the owner/group of the folder to the SSH user + data[:owner] ||= ssh_info[:username] + data[:group] ||= ssh_info[:username] + + machine.ui.detail(I18n.t( + "vagrant_sf_smb.mounting_single", + host: data[:hostpath].to_s, + guest: data[:guestpath].to_s)) + machine.guest.capability( + :mount_smb_shared_folder, data[:smb_id], data[:guestpath], data) + end + end + + def cleanup(machine, opts) + + end + + protected + + def load_host_ips + script_path = File.expand_path("../scripts/host_info.ps1", __FILE__) + r = Vagrant::Util::PowerShell.execute(script_path) + if r.exit_code != 0 + raise Errors::PowershellError, + script: script_path, + stderr: r.stderr + end + + JSON.parse(r.stdout)["ip_addresses"] + end + end + end +end diff --git a/templates/locales/synced_folder_smb.yml b/templates/locales/synced_folder_smb.yml new file mode 100644 index 000000000..451cb80af --- /dev/null +++ b/templates/locales/synced_folder_smb.yml @@ -0,0 +1,39 @@ +en: + vagrant_sf_smb: + mounting: |- + Mounting SMB shared folders... + mounting_single: |- + %{host} => %{guest} + warning_password: |- + You will be asked for the username and password to use to mount the + folders shortly. Please use the proper username/password of your + Windows account. + + errors: + define_share_failed: |- + Exporting an SMB share failed! Details about the failure are shown + below. Please inspect the error message and correct any problems. + + Host path: %{host} + + Stderr: %{stderr} + + Stdout: %{stdout} + no_routable_host_addr: |- + We couldn't detect an IP address that was routable to this + machine from the guest machine! Please verify networking is properly + setup in the guest machine and that it is able to access this + host. + + As another option, you can manually specify an IP for the machine + to mount from using the `smb_host` option to the synced folder. + powershell_error: |- + An error occurred while executing a PowerShell script. This error + is shown below. Please read the error message and see if this is + a configuration error with your system. If it is not, then please + report a bug. + + Script: %{script} + Error: + + %{stderr}