This includes updates for resolving all warnings provided by Ruby for deprecations and/or removed methods. It also enables support for Ruby 2.7 in the specification constraint as all 2.7 related warnings are resolved with this changeset.
553 lines
19 KiB
Ruby
553 lines
19 KiB
Ruby
require "digest/sha1"
|
|
require "log4r"
|
|
require "pathname"
|
|
require "uri"
|
|
|
|
require "vagrant/box_metadata"
|
|
require "vagrant/util/downloader"
|
|
require "vagrant/util/file_checksum"
|
|
require "vagrant/util/platform"
|
|
|
|
module Vagrant
|
|
module Action
|
|
module Builtin
|
|
# This middleware will download a remote box and add it to the
|
|
# given box collection.
|
|
class BoxAdd
|
|
# This is the size in bytes that if a file exceeds, is considered
|
|
# to NOT be metadata.
|
|
METADATA_SIZE_LIMIT = 20971520
|
|
|
|
# This is the amount of time to "resume" downloads if a partial box
|
|
# file already exists.
|
|
RESUME_DELAY = 24 * 60 * 60
|
|
|
|
def initialize(app, env)
|
|
@app = app
|
|
@logger = Log4r::Logger.new("vagrant::action::builtin::box_add")
|
|
@parser = URI::RFC2396_Parser.new
|
|
end
|
|
|
|
def call(env)
|
|
@download_interrupted = false
|
|
|
|
unless env[:box_name].nil?
|
|
begin
|
|
if URI.parse(env[:box_name]).kind_of?(URI::HTTP)
|
|
env[:ui].warn(I18n.t("vagrant.box_add_url_warn"))
|
|
end
|
|
rescue URI::InvalidURIError
|
|
# do nothing
|
|
end
|
|
end
|
|
|
|
url = Array(env[:box_url]).map do |u|
|
|
u = u.gsub("\\", "/")
|
|
if Util::Platform.windows? && u =~ /^[a-z]:/i
|
|
# On Windows, we need to be careful about drive letters
|
|
u = "file:///#{@parser.escape(u)}"
|
|
end
|
|
|
|
if u =~ /^[a-z0-9]+:.*$/i && !u.start_with?("file://")
|
|
# This is not a file URL... carry on
|
|
next u
|
|
end
|
|
|
|
# Expand the path and try to use that, if possible
|
|
p = File.expand_path(@parser.unescape(u.gsub(/^file:\/\//, "")))
|
|
p = Util::Platform.cygwin_windows_path(p)
|
|
next "file://#{@parser.escape(p.gsub("\\", "/"))}" if File.file?(p)
|
|
|
|
u
|
|
end
|
|
|
|
# If we received a shorthand URL ("mitchellh/precise64"),
|
|
# then expand it properly.
|
|
expanded = false
|
|
url.each_index do |i|
|
|
next if url[i] !~ /^[^\/]+\/[^\/]+$/
|
|
|
|
if !File.file?(url[i])
|
|
server = Vagrant.server_url env[:box_server_url]
|
|
raise Errors::BoxServerNotSet if !server
|
|
|
|
expanded = true
|
|
url[i] = "#{server}/#{url[i]}"
|
|
end
|
|
end
|
|
|
|
# Call the hook to transform URLs into authenticated URLs.
|
|
# In the case we don't have a plugin that does this, then it
|
|
# will just return the same URLs.
|
|
hook_env = env[:hook].call(
|
|
:authenticate_box_url, box_urls: url.dup)
|
|
authed_urls = hook_env[:box_urls]
|
|
if !authed_urls || authed_urls.length != url.length
|
|
raise "Bad box authentication hook, did not generate proper results."
|
|
end
|
|
|
|
# Test if any of our URLs point to metadata
|
|
is_metadata_results = authed_urls.map do |u|
|
|
begin
|
|
metadata_url?(u, env)
|
|
rescue Errors::DownloaderError => e
|
|
e
|
|
end
|
|
end
|
|
|
|
if expanded && url.length == 1
|
|
is_error = is_metadata_results.find do |b|
|
|
b.is_a?(Errors::DownloaderError)
|
|
end
|
|
|
|
if is_error
|
|
raise Errors::BoxAddShortNotFound,
|
|
error: is_error.extra_data[:message],
|
|
name: env[:box_url],
|
|
url: url
|
|
end
|
|
end
|
|
|
|
is_metadata = is_metadata_results.any? { |b| b === true }
|
|
if is_metadata && url.length > 1
|
|
raise Errors::BoxAddMetadataMultiURL,
|
|
urls: url.join(", ")
|
|
end
|
|
|
|
if is_metadata
|
|
url = [url.first, authed_urls.first]
|
|
add_from_metadata(url, env, expanded)
|
|
else
|
|
add_direct(url, env)
|
|
end
|
|
|
|
@app.call(env)
|
|
end
|
|
|
|
# Adds a box file directly (no metadata component, versioning,
|
|
# etc.)
|
|
#
|
|
# @param [Array<String>] urls
|
|
# @param [Hash] env
|
|
def add_direct(urls, env)
|
|
env[:ui].output(I18n.t("vagrant.box_adding_direct"))
|
|
|
|
name = env[:box_name]
|
|
if !name || name == ""
|
|
raise Errors::BoxAddNameRequired
|
|
end
|
|
|
|
if env[:box_version]
|
|
raise Errors::BoxAddDirectVersion
|
|
end
|
|
|
|
provider = env[:box_provider]
|
|
provider = Array(provider) if provider
|
|
|
|
box_add(
|
|
urls,
|
|
name,
|
|
"0",
|
|
provider,
|
|
nil,
|
|
env,
|
|
checksum: env[:box_checksum],
|
|
checksum_type: env[:box_checksum_type],
|
|
)
|
|
end
|
|
|
|
# Adds a box given that the URL is a metadata document.
|
|
#
|
|
# @param [String | Array<String>] url The URL of the metadata for
|
|
# the box to add. If this is an array, then it must be a two-element
|
|
# array where the first element is the original URL and the second
|
|
# element is an authenticated URL.
|
|
# @param [Hash] env
|
|
# @param [Bool] expanded True if the metadata URL was expanded with
|
|
# a Atlas server URL.
|
|
def add_from_metadata(url, env, expanded)
|
|
original_url = env[:box_url]
|
|
provider = env[:box_provider]
|
|
provider = Array(provider) if provider
|
|
version = env[:box_version]
|
|
|
|
authenticated_url = url
|
|
if url.is_a?(Array)
|
|
# We have both a normal URL and "authenticated" URL. Split
|
|
# them up.
|
|
authenticated_url = url[1]
|
|
url = url[0]
|
|
end
|
|
|
|
display_original_url = Util::CredentialScrubber.scrub(Array(original_url).first)
|
|
display_url = Util::CredentialScrubber.scrub(url)
|
|
|
|
env[:ui].output(I18n.t(
|
|
"vagrant.box_loading_metadata",
|
|
name: display_original_url))
|
|
if original_url != url
|
|
env[:ui].detail(I18n.t(
|
|
"vagrant.box_expanding_url", url: display_url))
|
|
end
|
|
|
|
metadata = nil
|
|
begin
|
|
metadata_path = download(
|
|
authenticated_url, env, json: true, ui: false)
|
|
return if @download_interrupted
|
|
|
|
File.open(metadata_path) do |f|
|
|
metadata = BoxMetadata.new(f)
|
|
end
|
|
rescue Errors::DownloaderError => e
|
|
raise if !expanded
|
|
raise Errors::BoxAddShortNotFound,
|
|
error: e.extra_data[:message],
|
|
name: display_original_url,
|
|
url: display_url
|
|
ensure
|
|
metadata_path.delete if metadata_path && metadata_path.file?
|
|
end
|
|
|
|
if env[:box_name] && metadata.name != env[:box_name]
|
|
raise Errors::BoxAddNameMismatch,
|
|
actual_name: metadata.name,
|
|
requested_name: env[:box_name]
|
|
end
|
|
|
|
metadata_version = metadata.version(
|
|
version || ">= 0", provider: provider)
|
|
if !metadata_version
|
|
if provider && !metadata.version(">= 0", provider: provider)
|
|
raise Errors::BoxAddNoMatchingProvider,
|
|
name: metadata.name,
|
|
requested: provider,
|
|
url: display_url
|
|
else
|
|
raise Errors::BoxAddNoMatchingVersion,
|
|
constraints: version || ">= 0",
|
|
name: metadata.name,
|
|
url: display_url,
|
|
versions: metadata.versions.join(", ")
|
|
end
|
|
end
|
|
|
|
metadata_provider = nil
|
|
if provider
|
|
# If a provider was specified, make sure we get that specific
|
|
# version.
|
|
provider.each do |p|
|
|
metadata_provider = metadata_version.provider(p)
|
|
break if metadata_provider
|
|
end
|
|
elsif metadata_version.providers.length == 1
|
|
# If we have only one provider in the metadata, just use that
|
|
# provider.
|
|
metadata_provider = metadata_version.provider(
|
|
metadata_version.providers.first)
|
|
else
|
|
providers = metadata_version.providers.sort
|
|
|
|
choice = 0
|
|
options = providers.map do |p|
|
|
choice += 1
|
|
"#{choice}) #{p}"
|
|
end.join("\n")
|
|
|
|
# We have more than one provider, ask the user what they want
|
|
choice = env[:ui].ask(I18n.t(
|
|
"vagrant.box_add_choose_provider",
|
|
options: options) + " ", prefix: false)
|
|
choice = choice.to_i if choice
|
|
while !choice || choice <= 0 || choice > providers.length
|
|
choice = env[:ui].ask(I18n.t(
|
|
"vagrant.box_add_choose_provider_again") + " ",
|
|
prefix: false)
|
|
choice = choice.to_i if choice
|
|
end
|
|
|
|
metadata_provider = metadata_version.provider(
|
|
providers[choice-1])
|
|
end
|
|
|
|
provider_url = metadata_provider.url
|
|
if provider_url != authenticated_url
|
|
# Authenticate the provider URL since we're using auth
|
|
hook_env = env[:hook].call(:authenticate_box_url, box_urls: [provider_url])
|
|
authed_urls = hook_env[:box_urls]
|
|
if !authed_urls || authed_urls.length != 1
|
|
raise "Bad box authentication hook, did not generate proper results."
|
|
end
|
|
provider_url = authed_urls[0]
|
|
end
|
|
|
|
box_add(
|
|
[[provider_url, metadata_provider.url]],
|
|
metadata.name,
|
|
metadata_version.version,
|
|
metadata_provider.name,
|
|
url,
|
|
env,
|
|
checksum: metadata_provider.checksum,
|
|
checksum_type: metadata_provider.checksum_type,
|
|
)
|
|
end
|
|
|
|
protected
|
|
|
|
# Shared helper to add a box once you know various details
|
|
# about it. Shared between adding via metadata or by direct.
|
|
#
|
|
# @param [Array<String>] urls
|
|
# @param [String] name
|
|
# @param [String] version
|
|
# @param [String] provider
|
|
# @param [Hash] env
|
|
# @return [Box]
|
|
def box_add(urls, name, version, provider, md_url, env, **opts)
|
|
env[:ui].output(I18n.t(
|
|
"vagrant.box_add_with_version",
|
|
name: name,
|
|
version: version,
|
|
providers: Array(provider).join(", ")))
|
|
|
|
# Verify the box we're adding doesn't already exist
|
|
if provider && !env[:box_force]
|
|
box = env[:box_collection].find(
|
|
name, provider, version)
|
|
if box
|
|
raise Errors::BoxAlreadyExists,
|
|
name: name,
|
|
provider: provider,
|
|
version: version
|
|
end
|
|
end
|
|
|
|
# Now we have a URL, we have to download this URL.
|
|
box = nil
|
|
begin
|
|
box_url = nil
|
|
|
|
urls.each do |url|
|
|
show_url = nil
|
|
if url.is_a?(Array)
|
|
show_url = url[1]
|
|
url = url[0]
|
|
end
|
|
|
|
begin
|
|
box_url = download(url, env, show_url: show_url)
|
|
break
|
|
rescue Errors::DownloaderError => e
|
|
# If we don't have multiple URLs, just raise the error
|
|
raise if urls.length == 1
|
|
|
|
env[:ui].error(I18n.t(
|
|
"vagrant.box_download_error", message: e.message))
|
|
box_url = nil
|
|
end
|
|
end
|
|
|
|
if opts[:checksum] && opts[:checksum_type]
|
|
if opts[:checksum].to_s.strip.empty?
|
|
@logger.warn("Given checksum is empty, cannot validate checksum for box")
|
|
elsif opts[:checksum_type].to_s.strip.empty?
|
|
@logger.warn("Given checksum type is empty, cannot validate checksum for box")
|
|
else
|
|
env[:ui].detail(I18n.t("vagrant.actions.box.add.checksumming"))
|
|
validate_checksum(
|
|
opts[:checksum_type], opts[:checksum], box_url)
|
|
end
|
|
end
|
|
|
|
# Add the box!
|
|
box = env[:box_collection].add(
|
|
box_url, name, version,
|
|
force: env[:box_force],
|
|
metadata_url: md_url,
|
|
providers: provider)
|
|
ensure
|
|
# Make sure we delete the temporary file after we add it,
|
|
# unless we were interrupted, in which case we keep it around
|
|
# so we can resume the download later.
|
|
if !@download_interrupted
|
|
@logger.debug("Deleting temporary box: #{box_url}")
|
|
begin
|
|
box_url.delete if box_url
|
|
rescue Errno::ENOENT
|
|
# Not a big deal, the temp file may not actually exist
|
|
end
|
|
end
|
|
end
|
|
|
|
env[:ui].success(I18n.t(
|
|
"vagrant.box_added",
|
|
name: box.name,
|
|
version: box.version,
|
|
provider: box.provider))
|
|
|
|
# Store the added box in the env for future middleware
|
|
env[:box_added] = box
|
|
|
|
box
|
|
end
|
|
|
|
# Returns the download options for the download.
|
|
#
|
|
# @return [Hash]
|
|
def downloader(url, env, **opts)
|
|
opts[:ui] = true if !opts.key?(:ui)
|
|
|
|
temp_path = env[:tmp_path].join("box" + Digest::SHA1.hexdigest(url))
|
|
@logger.info("Downloading box: #{url} => #{temp_path}")
|
|
|
|
if File.file?(url) || url !~ /^[a-z0-9]+:.*$/i
|
|
@logger.info("URL is a file or protocol not found and assuming file.")
|
|
file_path = File.expand_path(url)
|
|
file_path = Util::Platform.cygwin_windows_path(file_path)
|
|
file_path = file_path.gsub("\\", "/")
|
|
file_path = "/#{file_path}" if !file_path.start_with?("/")
|
|
url = "file://#{file_path}"
|
|
end
|
|
|
|
# If the temporary path exists, verify it is not too old. If its
|
|
# too old, delete it first because the data may have changed.
|
|
if temp_path.file?
|
|
delete = false
|
|
if env[:box_clean]
|
|
@logger.info("Cleaning existing temp box file.")
|
|
delete = true
|
|
elsif temp_path.mtime.to_i < (Time.now.to_i - RESUME_DELAY)
|
|
@logger.info("Existing temp file is too old. Removing.")
|
|
delete = true
|
|
end
|
|
|
|
temp_path.unlink if delete
|
|
end
|
|
|
|
downloader_options = {}
|
|
downloader_options[:ca_cert] = env[:box_download_ca_cert]
|
|
downloader_options[:ca_path] = env[:box_download_ca_path]
|
|
downloader_options[:continue] = true
|
|
downloader_options[:insecure] = env[:box_download_insecure]
|
|
downloader_options[:client_cert] = env[:box_download_client_cert]
|
|
downloader_options[:headers] = ["Accept: application/json"] if opts[:json]
|
|
downloader_options[:ui] = env[:ui] if opts[:ui]
|
|
downloader_options[:location_trusted] = env[:box_download_location_trusted]
|
|
downloader_options[:box_extra_download_options] = env[:box_extra_download_options]
|
|
|
|
Util::Downloader.new(url, temp_path, downloader_options)
|
|
end
|
|
|
|
def download(url, env, **opts)
|
|
opts[:ui] = true if !opts.key?(:ui)
|
|
|
|
d = downloader(url, env, **opts)
|
|
|
|
# Download the box to a temporary path. We store the temporary
|
|
# path as an instance variable so that the `#recover` method can
|
|
# access it.
|
|
if opts[:ui]
|
|
show_url = opts[:show_url]
|
|
show_url ||= url
|
|
display_url = Util::CredentialScrubber.scrub(show_url)
|
|
|
|
translation = "vagrant.box_downloading"
|
|
|
|
# Adjust status message when 'downloading' a local box.
|
|
if show_url.start_with?("file://")
|
|
translation = "vagrant.box_unpacking"
|
|
end
|
|
|
|
env[:ui].detail(I18n.t(
|
|
translation,
|
|
url: display_url))
|
|
if File.file?(d.destination)
|
|
env[:ui].info(I18n.t("vagrant.actions.box.download.resuming"))
|
|
end
|
|
end
|
|
|
|
begin
|
|
d.download!
|
|
rescue Errors::DownloaderInterrupted
|
|
# The downloader was interrupted, so just return, because that
|
|
# means we were interrupted as well.
|
|
@download_interrupted = true
|
|
env[:ui].info(I18n.t("vagrant.actions.box.download.interrupted"))
|
|
end
|
|
|
|
Pathname.new(d.destination)
|
|
end
|
|
|
|
# Tests whether the given URL points to a metadata file or a
|
|
# box file without completely downloading the file.
|
|
#
|
|
# @param [String] url
|
|
# @return [Boolean] true if metadata
|
|
def metadata_url?(url, env)
|
|
d = downloader(url, env, json: true, ui: false)
|
|
|
|
# If we're downloading a file, cURL just returns no
|
|
# content-type (makes sense), so we just test if it is JSON
|
|
# by trying to parse JSON!
|
|
uri = URI.parse(d.source)
|
|
if uri.scheme == "file"
|
|
url = uri.path
|
|
url ||= uri.opaque
|
|
#7570 Strip leading slash left in front of drive letter by uri.path
|
|
Util::Platform.windows? && url.gsub!(/^\/([a-zA-Z]:)/, '\1')
|
|
url = @parser.unescape(url)
|
|
|
|
begin
|
|
File.open(url, "r") do |f|
|
|
if f.size > METADATA_SIZE_LIMIT
|
|
# Quit early, don't try to parse the JSON of gigabytes
|
|
# of box files...
|
|
return false
|
|
end
|
|
|
|
BoxMetadata.new(f)
|
|
end
|
|
return true
|
|
rescue Errors::BoxMetadataMalformed
|
|
return false
|
|
rescue Errno::EINVAL
|
|
# Actually not sure what causes this, but its always
|
|
# in a case that isn't true.
|
|
return false
|
|
rescue Errno::EISDIR
|
|
return false
|
|
rescue Errno::ENOENT
|
|
return false
|
|
end
|
|
end
|
|
|
|
# If this isn't HTTP, then don't do the HEAD request
|
|
if !uri.scheme.downcase.start_with?("http")
|
|
@logger.info("not checking metadata since box URI isn't HTTP")
|
|
return false
|
|
end
|
|
|
|
output = d.head
|
|
match = output.scan(/^Content-Type: (.+?)$/i).last
|
|
return false if !match
|
|
!!(match.last.chomp =~ /application\/json/)
|
|
end
|
|
|
|
def validate_checksum(checksum_type, checksum, path)
|
|
@logger.info("Validating checksum with #{checksum_type}")
|
|
@logger.info("Expected checksum: #{checksum}")
|
|
|
|
actual = FileChecksum.new(path, checksum_type).checksum
|
|
@logger.info("Actual checksum: #{actual}")
|
|
if actual.casecmp(checksum) != 0
|
|
raise Errors::BoxChecksumMismatch,
|
|
actual: actual,
|
|
expected: checksum
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|