SMB enhancements

This commit is contained in:
Chris Roberts 2017-12-15 16:31:44 -08:00
parent e85ef1655b
commit 00fa50c296
15 changed files with 452 additions and 122 deletions

View File

@ -1,3 +1,4 @@
require "tmpdir"
require_relative "subprocess" require_relative "subprocess"
require_relative "which" require_relative "which"
@ -25,6 +26,10 @@ module Vagrant
# @return [Subprocess::Result] # @return [Subprocess::Result]
def self.execute(path, *args, **opts, &block) def self.execute(path, *args, **opts, &block)
validate_install! validate_install!
if opts.delete(:sudo) || opts.delete(:runas)
powerup_command(path, args, opts)
else
command = [ command = [
"powershell", "powershell",
"-NoLogo", "-NoLogo",
@ -41,6 +46,7 @@ module Vagrant
Subprocess.execute(*command, &block) Subprocess.execute(*command, &block)
end end
end
# Execute a powershell command. # Execute a powershell command.
# #
@ -56,7 +62,7 @@ module Vagrant
"-ExecutionPolicy", "Bypass", "-ExecutionPolicy", "Bypass",
"-Command", "-Command",
command command
].flatten ].flatten.compact
r = Subprocess.execute(*c) r = Subprocess.execute(*c)
return nil if r.exit_code != 0 return nil if r.exit_code != 0
@ -75,7 +81,7 @@ module Vagrant
"-NonInteractive", "-NonInteractive",
"-ExecutionPolicy", "Bypass", "-ExecutionPolicy", "Bypass",
"-Command", "-Command",
"$PSVersionTable.PSVersion.Major" "Write-Output $PSVersionTable.PSVersion.Major"
].flatten ].flatten
r = Subprocess.execute(*command) r = Subprocess.execute(*command)
@ -101,6 +107,60 @@ module Vagrant
end end
@_powershell_validation @_powershell_validation
end end
# Powerup the given command to perform privileged operations.
#
# @param [String] path
# @param [Array<String>] args
# @return [Array<String>]
def self.powerup_command(path, args, opts)
Dir.mktmpdir("vagrant") do |dpath|
all_args = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", path] + args
arg_list = "@('" + all_args.join("', '") + "')"
stdout = File.join(dpath, "stdout.txt")
stderr = File.join(dpath, "stderr.txt")
exitcode = File.join(dpath, "exitcode.txt")
script = "$sp = Start-Process -FilePath powershell -ArgumentList #{arg_list} " \
"-PassThru -Wait -RedirectStandardOutput '#{stdout}' -RedirectStandardError '#{stderr}' -WindowStyle Hidden; " \
"if($sp){ Set-Content -Path '#{exitcode}' -Value $sp.ExitCode;exit $sp.ExitCode; }else{ exit 1 }"
# escape quotes so we can nest our script within a start-process
script.gsub!("'", "''")
cmd = [
"powershell",
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-Command", "$p = Start-Process -FilePath powershell -ArgumentList " \
"@('-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', '#{script}') " \
"-PassThru -Wait -WindowStyle Hidden -Verb RunAs; if($p){ exit $p.ExitCode; }else{ exit 1 }"
]
result = Subprocess.execute(*cmd.push(opts))
if File.exist?(stdout)
r_stdout = File.read(stdout)
else
r_stdout = result.stdout
end
if File.exist?(stderr)
r_stderr = File.read(stderr)
else
r_stderr = result.stderr
end
code = 1
if File.exist?(exitcode)
code_txt = File.read(exitcode).strip
if code_txt.match(/^\d+$/)
code = code_txt.to_i
end
end
Subprocess::Result.new(code, r_stdout, r_stderr)
end
end
end end
end end
end end

View File

@ -24,7 +24,11 @@ module VagrantPlugins
smb_password = options[:smb_password] smb_password = options[:smb_password]
options[:mount_options] ||= [] options[:mount_options] ||= []
if machine.env.host.capability?(:smb_mount_options)
options[:mount_options] += machine.env.host.capability(:smb_mount_options)
else
options[:mount_options] << "sec=ntlm" options[:mount_options] << "sec=ntlm"
end
options[:mount_options] << "credentials=/etc/smb_creds_#{name}" options[:mount_options] << "credentials=/etc/smb_creds_#{name}"
mount_options = "-o uid=#{mount_uid},gid=#{mount_gid}" mount_options = "-o uid=#{mount_uid},gid=#{mount_gid}"

View File

@ -0,0 +1,18 @@
require "socket"
module VagrantPlugins
module HostDarwin
module Cap
class ConfiguredIPAddresses
def self.configured_ip_addresses(env)
Socket.getifaddrs.map do |interface|
if interface.addr.ipv4? && !interface.addr.ipv4_loopback?
interface.addr.ip_address
end
end.compact
end
end
end
end
end

View File

@ -0,0 +1,86 @@
module VagrantPlugins
module HostDarwin
module Cap
class SMB
@@logger = Log4r::Logger.new("vagrant::host::darwin::smb")
# If we have the sharing binary available, smb is installed
def self.smb_installed(env)
File.exist?("/usr/sbin/sharing")
end
# Required options for mounting a share hosted
# on macos.
def self.smb_mount_options(env)
["ver=3", "sec=ntlmssp", "nounix", "noperm"]
end
def self.smb_cleanup(env, machine, opts)
m_id = machine_id(machine)
result = Vagrant::Util::Subprocess.execute("/bin/sh", "-c",
"/usr/sbin/sharing -l | grep -E \"^name:.+\\svgt-#{m_id}-\" | awk '{print $2}'")
if result.exit_code != 0
@@logger.warn("failed to locate any shares for cleanup")
end
shares = result.stdout.split(/\s/).map(&:strip)
@@logger.debug("shares to be removed: #{shares}")
shares.each do |share_name|
@@logger.info("removing share name=#{share_name}")
share_name.strip!
result = Vagrant::Util::Subprocess.execute("/usr/bin/sudo",
"/usr/sbin/sharing", "-r", share_name)
if result.exit_code != 0
# Removing always returns 0 even if there are currently
# guests attached so if we get a non-zero value just
# log it as unexpected
@@logger.warn("removing share `#{share_name}` returned non-zero")
end
end
end
def self.smb_prepare(env, machine, folders, opts)
folders.each do |id, data|
hostpath = data[:hostpath]
chksum_id = Digest::MD5.hexdigest(id)
name = "vgt-#{machine_id(machine)}-#{chksum_id}"
data[:smb_id] ||= name
@@logger.info("creating new share name=#{name} id=#{data[:smb_id]}")
cmd = [
"/usr/bin/sudo",
"/usr/sbin/sharing",
"-a", hostpath,
"-S", data[:smb_id],
"-s", "001",
"-g", "000",
"-n", name
]
r = Vagrant::Util::Subprocess.execute(*cmd)
if r.exit_code != 0
raise Errors::DefineShareFailed,
host: hostpath.to_s,
stderr: r.stderr,
stdout: r.stdout
end
end
end
# Generates a unique identifier for the given machine
# based on the name, provider name, and working directory
# of the environment.
#
# @param [Vagrant::Machine] machine
# @return [String]
def self.machine_id(machine)
@@logger.debug("generating machine ID name=#{machine.name} cwd=#{machine.env.cwd}")
Digest::MD5.hexdigest("#{machine.name}-#{machine.provider_name}-#{machine.env.cwd}")
end
end
end
end
end

View File

@ -20,6 +20,31 @@ module VagrantPlugins
require_relative "cap/rdp" require_relative "cap/rdp"
Cap::RDP Cap::RDP
end end
host_capability("darwin", "smb_installed") do
require_relative "cap/smb"
Cap::SMB
end
host_capability("darwin", "smb_prepare") do
require_relative "cap/smb"
Cap::SMB
end
host_capability("darwin", "smb_mount_options") do
require_relative "cap/smb"
Cap::SMB
end
host_capability("darwin", "smb_cleanup") do
require_relative "cap/smb"
Cap::SMB
end
host_capability("darwin", "configured_ip_addresses") do
require_relative "cap/configured_ip_addresses"
Cap::ConfiguredIPAddresses
end
end end
end end
end end

View File

@ -0,0 +1,29 @@
require "pathname"
require "tempfile"
require "vagrant/util/downloader"
require "vagrant/util/file_checksum"
require "vagrant/util/powershell"
require "vagrant/util/subprocess"
module VagrantPlugins
module HostWindows
module Cap
class ConfiguredIPAddresses
def self.configured_ip_addresses(env)
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
res = JSON.parse(r.stdout)["ip_addresses"]
Array(res)
end
end
end
end
end

View File

@ -0,0 +1,102 @@
module VagrantPlugins
module HostWindows
module Cap
class SMB
# Number of seconds to display UAC warning to user
UAC_PROMPT_WAIT = 4
@@logger = Log4r::Logger.new("vagrant::host::windows::smb")
def self.smb_installed(env)
psv = Vagrant::Util::PowerShell.version.to_i
if psv < 3
if raise_error
raise SyncedFolderSMB::Errors::PowershellVersion,
version: psv.to_s
end
return false
end
true
end
def self.smb_cleanup(env, machine, opts)
script_path = File.expand_path("../../scripts/unset_share.ps1", __FILE__)
m_id = machine_id(machine)
result = Vagrant::Util::PowerShell.execute_cmd("net share")
if result.nil?
@@logger.warn("failed to get current share list")
return
end
prune_shares = result.split("\n").map do |line|
sections = line.split(/\s/)
if sections.first.to_s.start_with?("vgt-#{m_id}")
sections.first
end
end.compact
@@logger.debug("shares to be removed: #{prune_shares}")
if prune_shares.size > 0
machine.env.ui.warn("\n" + I18n.t("vagrant_sf_smb.uac.prune_warning") + "\n")
sleep UAC_PROMPT_WAIT
@@logger.info("remove shares: #{prune_shares}")
result = Vagrant::Util::PowerShell.execute(script_path, *prune_shares, sudo: true)
if result.exit_code != 0
failed_name = result.stdout.to_s.sub("share name: ", "")
raise SyncedFolderSMB::Errors::PruneShareFailed,
name: failed_name,
stderr: result.stderr,
stdout: result.stdout
end
end
end
def self.smb_prepare(env, machine, folders, opts)
script_path = File.expand_path("../../scripts/set_share.ps1", __FILE__)
shares = []
folders.each do |id, data|
hostpath = data[:hostpath]
chksum_id = Digest::MD5.hexdigest(id)
name = "vgt-#{machine_id(machine)}-#{chksum_id}"
data[:smb_id] ||= name
@@logger.info("creating new share name=#{name} id=#{data[:smb_id]}")
shares << [
"\"#{hostpath.gsub("/", "\\")}\"",
name,
data[:smb_id]
]
end
if !shares.empty?
machine.env.ui.warn("\n" + I18n.t("vagrant_sf_smb.uac.create_warning") + "\n")
sleep(UAC_PROMPT_WAIT)
result = Vagrant::Util::PowerShell.execute(script_path, *shares, sudo: true)
if result.exit_code != 0
share_path = result.stdout.to_s.sub("share path: ", "")
raise SyncedFolderSMB::Errors::DefineShareFailed,
host: share_path,
stderr: result.stderr,
stdout: result.stdout
end
end
end
# Generates a unique identifier for the given machine
# based on the name, provider name, and working directory
# of the environment.
#
# @param [Vagrant::Machine] machine
# @return [String]
def self.machine_id(machine)
@@logger.debug("generating machine ID name=#{machine.name} cwd=#{machine.env.cwd}")
Digest::MD5.hexdigest("#{machine.name}-#{machine.provider_name}-#{machine.env.cwd}")
end
end
end
end
end

View File

@ -30,6 +30,26 @@ module VagrantPlugins
require_relative "cap/ps" require_relative "cap/ps"
Cap::PS Cap::PS
end end
host_capability("windows", "smb_installed") do
require_relative "cap/smb"
Cap::SMB
end
host_capability("windows", "smb_prepare") do
require_relative "cap/smb"
Cap::SMB
end
host_capability("windows", "smb_cleanup") do
require_relative "cap/smb"
Cap::SMB
end
host_capability("windows", "configured_ip_addresses") do
require_relative "cap/configured_ip_addresses"
Cap::ConfiguredIPAddresses
end
end end
end end
end end

View File

@ -0,0 +1,39 @@
# The names of the user are language dependent!
$objSID = New-Object System.Security.Principal.SecurityIdentifier("S-1-1-0")
$objUser = $objSID.Translate([System.Security.Principal.NTAccount])
$grant = "$objUser,Full"
# First we split the defs string by commas to get
# each group of parameters
for ($i=0; $i -le $args.length; $i = $i + 3) {
$path = $args[$i]
$share_name = $args[$i+1]
$share_id = $args[$i+2]
if ($path -eq $null) {
Write-Warning "empty path argument encountered - complete"
exit 0
}
if ($share_name -eq $null) {
Write-Output "share path: ${path}"
Write-Error "error - no share name provided"
exit 1
}
if ($share_id -eq $null) {
Write-Output "share path: ${path}"
Write-Error "error - no share ID provided"
exit 1
}
$result = net share $share_name=$path /unlimited /GRANT:$grant /REMARK:"${share_id}"
if ($LastExitCode -ne 0) {
$host.ui.WriteLine("share path: ${path}")
$host.ui.WriteErrorLine("error ${result}")
exit 1
}
}
exit 0

View File

@ -0,0 +1,11 @@
# Share names are comma delimited
ForEach ($share_name in $args) {
$result = net share $share_name /DELETE
if ($LastExitCode -ne 0) {
Write-Output "share name: ${share_name}"
Write-Error "error - ${result}"
exit 1
}
}
Write-Output "share removal completed"
exit 0

View File

@ -10,6 +10,10 @@ module VagrantPlugins
error_key(:define_share_failed) error_key(:define_share_failed)
end end
class PruneShareFailed < SMBError
error_key(:prune_share_failed)
end
class NoHostIPAddr < SMBError class NoHostIPAddr < SMBError
error_key(:no_routable_host_addr) error_key(:no_routable_host_addr)
end end

View File

@ -1,44 +0,0 @@
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[Parameter(Mandatory=$true)]
[string]$share_name,
[string]$host_share_username = $null
)
$ErrorAction = "Stop"
if (net share | Select-String $share_name) {
net share $share_name /delete /y
}
# The names of the user are language dependent!
$objSID = New-Object System.Security.Principal.SecurityIdentifier("S-1-1-0")
$objUser = $objSID.Translate([System.Security.Principal.NTAccount])
$grant = "$objUser,Full"
if (![string]::IsNullOrEmpty($host_share_username)) {
$computer_name = $(Get-WmiObject Win32_Computersystem).name
$grant = "$computer_name\$host_share_username,Full"
# Here we need to set the proper ACL for this folder. This lets full
# recursive access to this folder.
<#
Get-ChildItem $path -recurse -Force |% {
$current_acl = Get-ACL $_.fullname
$permission = "$computer_name\$host_share_username","FullControl","ContainerInherit,ObjectInherit","None","Allow"
$acl_access_rule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$current_acl.SetAccessRule($acl_access_rule)
$current_acl | Set-Acl $_.fullname
}
#>
}
$result = net share $share_name=$path /unlimited /GRANT:$grant
if ($LastExitCode -eq 0) {
exit 0
}
$host.ui.WriteErrorLine("Error: $result")
exit 1

View File

@ -17,69 +17,46 @@ module VagrantPlugins
end end
def usable?(machine, raise_error=false) def usable?(machine, raise_error=false)
if !Vagrant::Util::Platform.windows? # If the machine explicitly states SMB is not supported, then
raise Errors::WindowsHostRequired if raise_error # believe it
return false return false if !machine.config.smb.functional
end return true if machine.env.host.capability?(:smb_installed) &&
machine.env.host.capability(:smb_installed)
if !Vagrant::Util::Platform.windows_admin? return false if !raise_error
raise Errors::WindowsAdminRequired if raise_error raise Vagrant::Errors::SMBNotSupported
return false
end
psv = Vagrant::Util::PowerShell.version.to_i
if psv < 3
if raise_error
raise Errors::PowershellVersion,
version: psv.to_s
end
return false
end
true
end end
def prepare(machine, folders, opts) def prepare(machine, folders, opts)
machine.ui.output(I18n.t("vagrant_sf_smb.preparing")) machine.ui.output(I18n.t("vagrant_sf_smb.preparing"))
script_path = File.expand_path("../scripts/set_share.ps1", __FILE__) smb_username = smb_password = nil
# If we need auth information, then ask the user. # If we need auth information, then ask the user.
have_auth = false have_auth = false
folders.each do |id, data| folders.each do |id, data|
if data[:smb_username] && data[:smb_password] if data[:smb_username] && data[:smb_password]
@creds[:username] = data[:smb_username] smb_username = data[:smb_username]
@creds[:password] = data[:smb_password] smb_password = data[:smb_password]
have_auth = true have_auth = true
break break
end end
end end
if !have_auth if !have_auth
machine.ui.detail(I18n.t("vagrant_sf_smb.warning_password") + "\n ") machine.env.ui.detail(I18n.t("vagrant_sf_smb.warning_password") + "\n ")
@creds[:username] = machine.ui.ask("Username: ") smb_username = machine.env.ui.ask("Username: ")
@creds[:password] = machine.ui.ask("Password (will be hidden): ", echo: false) smb_password = machine.env.ui.ask("Password (will be hidden): ", echo: false)
end end
folders.each do |id, data| folders.each do |id, data|
hostpath = data[:hostpath] data[:smb_username] ||= smb_username
data[:smb_password] ||= smb_password
data[:smb_id] ||= Digest::MD5.hexdigest( # Register password as sensitive
"#{machine.id}-#{id.gsub("/", "-")}") Vagrant::Util::CredentialScrubber.sensitive(smb_password)
args = []
args << "-path" << "\"#{hostpath.gsub("/", "\\")}\""
args << "-share_name" << data[:smb_id]
#args << "-host_share_username" << @creds[:username]
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
machine.env.host.capability(:smb_prepare, machine, folders, opts)
end end
def enable(machine, folders, nfsopts) def enable(machine, folders, nfsopts)
@ -109,7 +86,7 @@ module VagrantPlugins
end end
if need_host_ip if need_host_ip
candidate_ips = load_host_ips candidate_ips = machine.env.host.capability(:configured_ip_addresses)
@logger.debug("Potential host IPs: #{candidate_ips.inspect}") @logger.debug("Potential host IPs: #{candidate_ips.inspect}")
host_ip = machine.guest.capability( host_ip = machine.guest.capability(
:choose_addressable_ip_addr, candidate_ips) :choose_addressable_ip_addr, candidate_ips)
@ -141,25 +118,8 @@ module VagrantPlugins
end end
def cleanup(machine, opts) def cleanup(machine, opts)
if machine.env.host.capability?(:smb_cleanup)
end machine.env.host.capability(:smb_cleanup, machine, opts)
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
res = JSON.parse(r.stdout)["ip_addresses"]
if res.instance_of? String
[ res ]
else
res
end end
end end
end end

View File

@ -11,6 +11,13 @@ en:
folders shortly. Please use the proper username/password of your folders shortly. Please use the proper username/password of your
Windows account. Windows account.
uac:
prune_warning: |-
Vagrant requires administator access for pruning SMB shares and
may request access to complete removal of stale shares.
create_warning: |-
Vagrant requires administator access to create SMB shares and
may request access to complete setup of configured shares.
errors: errors:
define_share_failed: |- define_share_failed: |-
Exporting an SMB share failed! Details about the failure are shown Exporting an SMB share failed! Details about the failure are shown
@ -20,6 +27,15 @@ en:
Stderr: %{stderr} Stderr: %{stderr}
Stdout: %{stdout}
prune_share_failed: |-
Pruning an SMB share failed! Details about the failure are shown
below. Please inspect the error message and correct any problems.
Share name: %{name}
Stderr: %{stderr}
Stdout: %{stdout} Stdout: %{stdout}
no_routable_host_addr: |- no_routable_host_addr: |-
We couldn't detect an IP address that was routable to this We couldn't detect an IP address that was routable to this