vaguerent/lib/vagrant/util/platform.rb
Chris Roberts 51adb12547 Add architecture support for boxes
Introduce support for handling box architecture. Adds a new
`box_architecture` setting that defaults to `:auto` which will perform
automatic detection of the host system, but can be overridden with a
custom value. Can also be set to `nil` which will result in it fetching
the box flagged with the default architecture within the metadata.

Box collection has been modified to allow existing boxes already
downloaded and unpacked to still function as expected when architecture
information is not available.
2023-09-14 16:15:03 -07:00

738 lines
27 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require "rbconfig"
require "shellwords"
require "tempfile"
require "tmpdir"
require "log4r"
require "vagrant/util/subprocess"
require "vagrant/util/powershell"
require "vagrant/util/which"
module Vagrant
module Util
# This class just contains some platform checking code.
class Platform
class << self
# Detect architecture of host system
#
# @return [String]
def architecture
if !defined?(@_host_architecture)
if ENV["VAGRANT_HOST_ARCHITECTURE"].to_s != ""
return @_host_architecture = ENV["VAGRANT_HOST_ARCHITECTURE"]
end
@_host_architecture = case RbConfig::CONFIG["target_cpu"]
when "x86_64"
"amd64"
when "i386"
"386"
when "arm64", "aarch64"
"arm64"
else
RbConfig::CONFIG["target_cpu"]
end
end
@_host_architecture
end
def logger
if !defined?(@_logger)
@_logger = Log4r::Logger.new("vagrant::util::platform")
end
@_logger
end
def cygwin?
if !defined?(@_cygwin)
@_cygwin = ENV["VAGRANT_DETECTED_OS"].to_s.downcase.include?("cygwin") ||
platform.include?("cygwin") ||
ENV["OSTYPE"].to_s.downcase.include?("cygwin")
end
@_cygwin
end
def msys?
if !defined?(@_msys)
@_msys = ENV["VAGRANT_DETECTED_OS"].to_s.downcase.include?("msys") ||
platform.include?("msys") ||
ENV["OSTYPE"].to_s.downcase.include?("msys")
end
@_msys
end
def wsl?
if !defined?(@_wsl)
@_wsl = false
SilenceWarnings.silence! do
# Find 'microsoft' in /proc/version indicative of WSL
if File.file?('/proc/version')
osversion = File.open('/proc/version', &:gets)
if osversion.downcase.include?("microsoft")
@_wsl = true
end
end
end
end
@_wsl
end
[:darwin, :bsd, :freebsd, :linux, :solaris].each do |type|
define_method("#{type}?") do
platform.include?(type.to_s)
end
end
def windows?
return @_windows if defined?(@_windows)
@_windows = %w[mingw mswin].any? { |t| platform.include?(t) }
return @_windows
end
# Checks if the user running Vagrant on Windows has administrative
# privileges.
#
# From: https://support.microsoft.com/en-us/kb/243330
# SID: S-1-5-19
#
# @return [Boolean]
def windows_admin?
return @_windows_admin if defined?(@_windows_admin)
@_windows_admin = -> {
ps_cmd = '(new-object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)'
output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd)
return output == 'True'
}.call
return @_windows_admin
end
# Checks if Hyper-V is accessible to the local user. It will check
# if user is in the "Hyper-V Administrators" group, is a Domain
# administrator, and finally will run a manual interaction with
# Hyper-V to determine if Hyper-V is usable for the current user.
#
# From: https://support.microsoft.com/en-us/kb/243330
# SID: S-1-5-32-578
# Name: BUILTIN\Hyper-V Administrators
# SID: S-1-5-21DOMAIN-512
# Name: Domain Admins
#
# @return [Boolean]
def windows_hyperv_admin?
return @_windows_hyperv_admin if defined?(@_windows_hyperv_admin)
if ENV["VAGRANT_IS_HYPERV_ADMIN"]
return @_windows_hyperv_admin = true
end
ps_cmd = "Write-Output ([System.Security.Principal.WindowsIdentity]::GetCurrent().Groups | " \
"Select-Object Value | ConvertTo-JSON)"
output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd)
if output
groups = begin
JSON.load(output)
rescue JSON::ParserError
[]
end
admin_group = groups.detect do |g|
g["Value"].to_s == "S-1-5-32-578" ||
(g["Value"].start_with?("S-1-5-21") && g["Value"].to_s.end_with?("-512"))
end
if admin_group
return @_windows_hyperv_admin = true
end
end
ps_cmd = "$x = (Get-VMHost).Name; if($x -eq [System.Net.Dns]::GetHostName()){ Write-Output 'true'}"
output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd)
result = output == "true"
return @_windows_hyperv_admin = result
end
# Checks if Hyper-V is enabled on the host system and returns true
# if enabled.
#
# @return [Boolean]
def windows_hyperv_enabled?
return @_windows_hyperv_enabled if defined?(@_windows_hyperv_enabled)
@_windows_hyperv_enabled = -> {
check_commands = Array.new.tap do |c|
c << "(Get-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V-Hypervisor -Online).State"
c << "(Get-WindowsFeature -FeatureName Microsoft-Hyper-V-Hypervisor).State"
end
check_commands.each do |ps_cmd|
begin
output = Vagrant::Util::PowerShell.execute_cmd(ps_cmd)
return true if output == "Enabled"
rescue Errors::PowerShellInvalidVersion
logger.warn("Invalid PowerShell version detected during Hyper-V enable check")
return false
rescue Errors::PowerShellError
logger.warn("Powershell command not found or error on execution of command")
return false
end
end
return false
}.call
return @_windows_hyperv_enabled
end
# This takes any path and converts it from a Windows path to a
# Cygwin style path.
#
# @param [String] path
# @return [String]
def cygwin_path(path)
begin
cygpath = Vagrant::Util::Which.which("cygpath")
if cygpath.nil?
# If Which can't find it, just attempt to invoke it directly
cygpath = "cygpath"
else
cygpath.gsub!("/", '\\')
end
process = Subprocess.execute(
cygpath, "-u", "-a", path.to_s)
return process.stdout.chomp
rescue Errors::CommandUnavailableWindows => e
# Sometimes cygpath isn't available (msys). Instead, do what we
# can with bash tricks.
process = Subprocess.execute(
"bash",
"--noprofile",
"--norc",
"-c", "cd #{Shellwords.escape(path)} && pwd")
return process.stdout.chomp
end
end
# This takes any path and converts it from a Windows path to a
# msys style path.
#
# @param [String] path
# @return [String]
def msys_path(path)
begin
# We have to revert to the old env
# path here, otherwise it looks like
# msys2 ends up using the wrong cygpath
# binary and ends up with a `/cygdrive`
# when it doesn't exist in msys2
original_path_env = ENV['PATH']
ENV['PATH'] = ENV['VAGRANT_OLD_ENV_PATH']
cygwin_path(path)
ensure
ENV['PATH'] = original_path_env
end
end
# This takes any path and converts it to a full-length Windows
# path on Windows machines in Cygwin.
#
# @return [String]
def cygwin_windows_path(path)
return path if !cygwin?
# Replace all "\" with "/", otherwise cygpath doesn't work.
path = unix_windows_path(path)
# Call out to cygpath and gather the result
process = Subprocess.execute("cygpath", "-w", "-l", "-a", path.to_s)
return process.stdout.chomp
end
# This takes any path and converts Windows-style path separators
# to Unix-like path separators.
# @return [String]
def unix_windows_path(path)
path.gsub("\\", "/")
end
# This checks if the filesystem is case sensitive. This is not a
# 100% correct check, since it is possible that the temporary
# directory runs a different filesystem than the root directory.
# However, this works in many cases.
def fs_case_sensitive?
return @_fs_case_sensitive if defined?(@_fs_case_sensitive)
@_fs_case_sensitive = Dir.mktmpdir("vagrant-fs-case-sensitive") do |dir|
tmp_file = File.join(dir, "FILE")
File.open(tmp_file, "w") do |f|
f.write("foo")
end
# The filesystem is case sensitive if the lowercased version
# of the filename is NOT reported as existing.
!File.file?(File.join(dir, "file"))
end
return @_fs_case_sensitive
end
# This expands the path and ensures proper casing of each part
# of the path.
def fs_real_path(path, **opts)
path = Pathname.new(File.expand_path(path))
if path.exist? && !fs_case_sensitive?
# If the path contains a Windows short path, then we attempt to
# expand. The require below is embedded here since it requires
# windows to work.
if windows? && path.to_s =~ /~\d(\/|\\)/
require_relative "windows_path"
path = Pathname.new(WindowsPath.longname(path.to_s))
end
# Build up all the parts of the path
original = []
while !path.root?
original.unshift(path.basename.to_s)
path = path.parent
end
# Traverse each part and join it into the resulting path
original.each do |single|
Dir.entries(path).each do |entry|
begin
single = single.encode("filesystem").to_s
rescue ArgumentError => err
Vagrant.global_logger.warn("path encoding failed - part=#{single} err=#{err.class} msg=#{err}")
# NOTE: Depending on the Windows environment the above
# encode will generate an "input string invalid" when
# attempting to encode. If that happens, continue on
end
if entry.downcase == single.downcase
path = path.join(entry)
end
end
end
end
if windows?
# Fix the drive letter to be uppercase.
path = path.to_s
if path[1] == ":"
path[0] = path[0].upcase
end
path = Pathname.new(path)
end
path
end
# Converts a given path to UNC format by adding a prefix and converting slashes.
# @param [String] path Path to convert to UNC for Windows
# @return [String]
def windows_unc_path(path)
path = path.gsub("/", "\\")
# Convert to UNC path
if path =~ /^[a-zA-Z]:\\?$/
# If the path is just a drive letter, then return that as-is
path + "\\"
elsif path.start_with?("\\\\")
# If the path already starts with `\\` assume UNC and return as-is
path
else
"\\\\?\\" + path.gsub("/", "\\")
end
end
# Returns a boolean noting whether the terminal supports color.
# output.
def terminal_supports_colors?
return @_terminal_supports_colors if defined?(@_terminal_supports_colors)
@_terminal_supports_colors = -> {
if windows?
return true if ENV.key?("ANSICON")
return true if cygwin?
return true if ENV["TERM"] == "cygwin"
return false
end
return true
}.call
return @_terminal_supports_colors
end
def platform
return @_platform if defined?(@_platform)
@_platform = RbConfig::CONFIG["host_os"].downcase
return @_platform
end
# Determine if given path is within the WSL rootfs. Returns
# true if within the subsystem, or false if outside the subsystem.
#
# @param [String] path Path to check
# @return [Boolean] path is within subsystem
def wsl_path?(path)
wsl? && !path.to_s.downcase.start_with?("/mnt/")
end
# Compute the path to rootfs of currently active WSL.
#
# @return [String] A path to rootfs of a current WSL instance.
def wsl_rootfs
return @_wsl_rootfs if defined?(@_wsl_rootfs)
if wsl?
# Mark our filesystem with a temporary file having an unique name.
marker = Tempfile.new(Time.now.to_i.to_s)
logger = Log4r::Logger.new("vagrant::util::platform::wsl")
# Check for lxrun installation first
lxrun_path = [wsl_windows_appdata_local, "lxss"].join("\\")
paths = [lxrun_path]
logger.debug("checking registry for WSL installation path")
paths += PowerShell.execute_cmd(
'(Get-ChildItem HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss ' \
'| ForEach-Object {Get-ItemProperty $_.PSPath}).BasePath').to_s.split("\r\n").map(&:strip)
paths.delete_if{|path| path.to_s.empty?}
paths.each do |path|
# Lowercase the drive letter, skip the next symbol (which is a
# colon from a Windows path) and convert path to UNIX style.
check_path = "/mnt/#{path[0, 1].downcase}#{path[2..-1].tr('\\', '/')}/rootfs"
begin
process = Subprocess.execute("wslpath", "-u", "-a", path)
check_path = "#{process.stdout.chomp}/rootfs" if process.exit_code == 0
rescue Errors::CommandUnavailable => e
# pass
end
logger.debug("checking `#{path}` for current WSL instance")
begin
# https://blogs.msdn.microsoft.com/wsl/2016/06/15/wsl-file-system-support
# Current WSL instance doesn't have an access to its mount from
# within itself despite all others are available. That's the
# hacky way we're using to determine current instance.
# For example we have three WSL instances:
# A -> C:\User\USER\AppData\Local\Packages\A\LocalState\rootfs
# B -> C:\User\USER\AppData\Local\Packages\B\LocalState\rootfs
# C -> C:\User\USER\AppData\Local\Packages\C\LocalState\rootfs
# If we're in "A" WSL at the moment, then its path will not be
# accessible since it's mounted for exactly the instance we're
# in. All others can be opened.
Dir.open(check_path) do |fs|
# A fallback for a case if our trick will stop working. For
# that we've created a temporary file with an unique name in
# a current WSL and now seeking it among all WSL.
if File.exist?("#{fs.path}/#{marker.path}")
@_wsl_rootfs = path
break
end
end
rescue Errno::EACCES
@_wsl_rootfs = path
# You can create and simultaneously run multiple WSL instances,
# comment out the "break", run this script within each one and
# it'll return only single value.
break
rescue Errno::ENOENT
# Warn about data discrepancy between Winreg and file system
# states. For the sake of justice, it's worth mentioning that
# it is possible only when someone will manually break WSL by
# removing a directory of its base path (kinda "stupid WSL
# uninstallation by removing hidden and system directory").
logger.warn("WSL instance at `#{path} is broken or no longer exists")
end
# All other exceptions have to be raised since they will mean
# something unpredictably terrible.
end
marker.close!
raise Vagrant::Errors::WSLRootFsNotFoundError if @_wsl_rootfs.nil?
end
# Attach the rootfs leaf to the path
if @_wsl_rootfs != lxrun_path
@_wsl_rootfs = "#{@_wsl_rootfs}\\rootfs"
end
logger.debug("detected `#{@_wsl_rootfs}` as current WSL instance")
@_wsl_rootfs
end
# Convert a WSL path to the local Windows path. This is useful
# for conversion when calling out to Windows executables from
# the WSL
#
# @param [String, Pathname] path Path to convert
# @return [String]
def wsl_to_windows_path(path)
path = path.to_s
if wsl? && wsl_windows_access? && !path.match(/^[a-zA-Z]:/)
path = File.expand_path(path)
begin
process = Subprocess.execute("wslpath", "-w", "-a", path)
return process.stdout.chomp if process.exit_code == 0
rescue Errors::CommandUnavailable => e
# pass
end
if wsl_path?(path)
parts = path.split("/")
parts.delete_if(&:empty?)
root_path = wsl_rootfs
# lxrun splits home separate so we need to account
# for it's specialness here when we build the path
if root_path.end_with?("lxss") && !(["root", "home"].include?(parts.first))
root_path = "#{root_path}\\rootfs"
end
path = [root_path, *parts].join("\\")
else
path = path.sub("/mnt/", "")
parts = path.split("/")
parts.first << ":"
path = parts.join("\\")
end
end
path
end
# Takes a windows path and formats it to the
# 'unix' style (i.e. `/cygdrive/c` or `/c/`)
#
# @param [Pathname, String] path Path to convert
# @param [Hash] hash of arguments
# @return [String]
def format_windows_path(path, *args)
path = cygwin_path(path) if cygwin?
path = msys_path(path) if msys?
path = wsl_to_windows_path(path) if wsl?
if windows? || wsl?
path = windows_unc_path(path) if !args.include?(:disable_unc)
end
path
end
# Automatically convert a given path to a Windows path. Will only
# be applied if running on a Windows host. If running on Windows
# host within the WSL, the actual Windows path will be returned.
#
# @param [Pathname, String] path Path to convert
# @return [String]
def windows_path(path)
path = cygwin_windows_path(path)
path = wsl_to_windows_path(path)
if windows? || wsl?
path = windows_unc_path(path)
end
path
end
# Allow Vagrant to access Vagrant managed machines outside the
# Windows Subsystem for Linux
#
# @return [Boolean]
def wsl_windows_access?
if !defined?(@_wsl_windows_access)
@_wsl_windows_access = wsl? && ENV["VAGRANT_WSL_ENABLE_WINDOWS_ACCESS"]
end
@_wsl_windows_access
end
# The allowed windows system path Vagrant can manage from the Windows
# Subsystem for Linux
#
# @return [Pathname]
def wsl_windows_accessible_path
if !defined?(@_wsl_windows_accessible_path)
access_path = ENV["VAGRANT_WSL_WINDOWS_ACCESS_USER_HOME_PATH"]
if access_path.to_s.empty?
begin
process = Subprocess.execute("wslpath", "-u", "-a", wsl_windows_home)
access_path = process.stdout.chomp if process.exit_code == 0
rescue Errors::CommandUnavailable => e
# pass
end
end
if access_path.to_s.empty?
access_path = wsl_windows_home.gsub("\\", "/").sub(":", "")
access_path[0] = access_path[0].downcase
access_path = "/mnt/#{access_path}"
end
@_wsl_windows_accessible_path = Pathname.new(access_path)
end
@_wsl_windows_accessible_path
end
# Checks given path to determine if Vagrant is allowed to bypass checks
#
# @param [String] path Path to check
# @return [Boolean] Vagrant is allowed to bypass checks
def wsl_windows_access_bypass?(path)
wsl? && wsl_windows_access? &&
path.to_s.start_with?(wsl_windows_accessible_path.to_s)
end
# Mount pattern for extracting local mount information
MOUNT_PATTERN = /^(?<device>.+?) on (?<mount>.+?) type (?<type>.+?) \((?<options>.+)\)/.freeze
# Get list of local mount paths that are DrvFs file systems
#
# @return [Array<String>]
# @todo(chrisroberts): Constantize types for check
def wsl_drvfs_mounts
if !defined?(@_wsl_drvfs_mounts)
@_wsl_drvfs_mounts = []
if wsl?
result = Util::Subprocess.execute("mount")
result.stdout.each_line do |line|
info = line.match(MOUNT_PATTERN)
if info && (info[:type] == "drvfs" || info[:type] == "9p")
@_wsl_drvfs_mounts << info[:mount]
end
end
end
end
@_wsl_drvfs_mounts
end
# Check if given path is located on DrvFs file system
#
# @param [String, Pathname] path Path to check
# @return [Boolean]
def wsl_drvfs_path?(path)
if wsl?
wsl_drvfs_mounts.each do |mount_path|
return true if path.to_s.start_with?(mount_path)
end
end
false
end
# If running within the Windows Subsystem for Linux, this will provide
# simple setup to allow sharing of the user's VAGRANT_HOME directory
# within the subsystem
#
# @param [Environment] env
# @param [Logger] logger Optional logger to display information
def wsl_init(env, logger=nil)
if wsl?
if ENV["VAGRANT_WSL_ENABLE_WINDOWS_ACCESS"]
wsl_validate_matching_vagrant_versions!
shared_user = ENV["VAGRANT_WSL_WINDOWS_ACCESS_USER"]
if shared_user.to_s.empty?
shared_user = wsl_windows_username
end
if logger
logger.warn("Windows Subsystem for Linux detected. Allowing access to user: #{shared_user}")
logger.warn("Vagrant will be allowed to control Vagrant managed machines within the user's home path.")
end
if ENV["VAGRANT_HOME"] || ENV["VAGRANT_WSL_DISABLE_VAGRANT_HOME"]
logger.warn("VAGRANT_HOME environment variable already set. Not overriding!") if logger
else
home_path = wsl_windows_accessible_path.to_s
ENV["VAGRANT_HOME"] = File.join(home_path, ".vagrant.d")
if logger
logger.info("Overriding VAGRANT_HOME environment variable to configured windows user. (#{ENV["VAGRANT_HOME"]})")
end
true
end
else
if env.local_data_path.to_s.start_with?("/mnt/")
raise Vagrant::Errors::WSLVagrantAccessError
end
end
end
end
# Fetch the Windows username currently in use
#
# @return [String, Nil]
def wsl_windows_username
if !@_wsl_windows_username
result = Util::Subprocess.execute("cmd.exe", "/c", "echo %USERNAME%")
if result.exit_code == 0
@_wsl_windows_username = result.stdout.strip
end
end
@_wsl_windows_username
end
# Fetch the Windows user home directory
#
# @return [String, Nil]
def wsl_windows_home
if !@_wsl_windows_home
result = Util::Subprocess.execute("cmd.exe", "/c" "echo %USERPROFILE%")
if result.exit_code == 0
@_wsl_windows_home = result.stdout.gsub("\"", "").strip
end
end
@_wsl_windows_home
end
# Fetch the Windows user local app data directory
#
# @return [String, Nil]
def wsl_windows_appdata_local
if !@_wsl_windows_appdata_local
result = Util::Subprocess.execute("cmd.exe", "/c", "echo %LOCALAPPDATA%")
if result.exit_code == 0
@_wsl_windows_appdata_local = result.stdout.gsub("\"", "").strip
end
end
@_wsl_windows_appdata_local
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!
valid = false
if Util::Which.which("vagrant.exe")
result = Util::Subprocess.execute("vagrant.exe", "--version")
if result.exit_code == 0
windows_version = result.stdout.match(/Vagrant (?<version>[\w.-]+)/)
if windows_version
windows_version = windows_version[:version].strip
valid = windows_version == Vagrant::VERSION
end
end
if !valid
raise Vagrant::Errors::WSLVagrantVersionMismatch,
wsl_version: Vagrant::VERSION,
windows_version: windows_version || "unknown"
end
end
end
# systemd is in use
def systemd?
if !defined?(@_systemd)
if !windows?
result = Vagrant::Util::Subprocess.execute("ps", "-o", "comm=", "1")
@_systemd = result.stdout.chomp == "systemd"
else
@_systemd = false
end
end
@_systemd
end
# @private
# Reset the cached values for platform. This is not considered a public
# API and should only be used for testing.
def reset!
instance_variables.each(&method(:remove_instance_variable))
end
end
end
end
end