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

374 lines
13 KiB
Ruby

# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
module VagrantPlugins
module CloudCommand
module Util
# @return [String] Vagrant Cloud server URL
def api_server_url
if Vagrant.server_url == Vagrant::DEFAULT_SERVER_URL
return "#{Vagrant.server_url}/api/v1"
end
begin
addr = URI.parse(Vagrant.server_url)
if addr.path.empty? || addr.path.to_s == "/"
addr.path = "/api/v1"
end
addr.to_s
rescue URI::Error
Vagrant.server_url
end
end
# @param [Vagrant::Environment] env
# @param [Hash] options
# @option options [String] :login Username or email
# @option options [String] :description Description of login usage for token
# @option options [String] :code 2FA code for login
# @option options [Boolean] :quiet Do not prompt user
# @returns [VagrantPlugins::CloudCommand::Client, nil]
def client_login(env, options={})
return @_client if defined?(@_client)
@_client = Client.new(env)
return @_client if @_client.logged_in?
# If directed to be quiet, do not continue and
# just return nil
return if options[:quiet]
# Let the user know what is going on.
env.ui.output(I18n.t("cloud_command.command_header") + "\n")
# If it is a private cloud installation, show that
if Vagrant.server_url != Vagrant::DEFAULT_SERVER_URL
env.ui.output("Vagrant Cloud URL: #{Vagrant.server_url}")
end
options = {} if !options
# Ask for the username
if options[:login]
@_client.username_or_email = options[:login]
env.ui.output("Vagrant Cloud username or email: #{@_client.username_or_email}")
else
@_client.username_or_email = env.ui.ask("Vagrant Cloud username or email: ")
end
@_client.password = env.ui.ask("Password (will be hidden): ", echo: false)
description_default = "Vagrant login from #{Socket.gethostname}"
if !options[:description]
description = env.ui.ask("Token description (Defaults to #{description_default.inspect}): ")
else
description = options[:description]
env.ui.output("Token description: #{description}")
end
description = description_default if description.empty?
code = nil
begin
token = @_client.login(description: description, code: code)
rescue Errors::TwoFactorRequired
until code
code = env.ui.ask("2FA code: ")
if @_client.two_factor_delivery_methods.include?(code.downcase)
delivery_method, code = code, nil
@_client.request_code delivery_method
end
end
retry
end
@_client.store_token(token)
Vagrant::Util::CredentialScrubber.sensitive(token)
env.ui.success(I18n.t("cloud_command.logged_in"))
@_client
end
# Print search results from Vagrant Cloud to the console
#
# @param [Array<VagrantCloud::Box>] search_results Box search results from Vagrant Cloud
# @param [Boolean] short Print short summary
# @param [Boolean] json Print output in JSON format
# @param [Vagrant::Environment] env Current Vagrant environment
# @return [nil]
def format_search_results(search_results, short, json, env)
result = search_results.map do |b|
{
name: b.tag,
version: b.current_version.version,
downloads: format_downloads(b.downloads.to_s),
providers: b.current_version.providers.map(&:name).uniq.join(", "),
architectures: b.current_version.providers.map(&:architecture).join(", ")
}
end
if short
result.map { |b| env.ui.info(b[:name]) }
elsif json
env.ui.info(result.to_json)
else
column_labels = {}
columns = result.first.keys
columns.each do |c|
column_labels[c] = c.to_s.upcase
end
print_search_table(env, column_labels, result, [:downloads])
end
nil
end
# Output box details result from Vagrant Cloud
#
# @param [VagrantCloud::Box, VagrantCloud::Box::Version] box Box or box version to display
# @param [Vagrant::Environment] env Current Vagrant environment
# @return [nil]
def format_box_results(box, env, options={})
if box.is_a?(VagrantCloud::Box)
info = box_info(box, options)
elsif box.is_a?(VagrantCloud::Box::Version)
info = version_info(box)
else
info = provider_info(box)
end
width = info.keys.map(&:size).max
info.each do |k, v|
v.to_s.split("\n").each_with_index do |line, idx|
whitespace = width - k.size + line.to_s.size
if idx == 0
env.ui.info "#{k}: #{line.rjust(whitespace)}"
else
whitespace += k.size + 2
env.ui.info line.rjust(whitespace)
end
end
end
nil
end
# Load box and yield
#
# @param [VagrantCloud::Account] account Vagrant Cloud account
# @param [String] org Organization name
# @param [String] box Box name
# @yieldparam [VagrantCloud::Box] box Requested Vagrant Cloud box
# @yieldreturn [Integer]
# @return [Integer]
def with_box(account:, org:, box:)
org = account.organization(name: org)
b = org.boxes.detect { |b| b.name == box }
if !b
@env.ui.error(I18n.t("cloud_command.box.not_found",
org: org.username, box_name: box))
return 1
end
yield b
end
# Load box version and yield
#
# @param [VagrantCloud::Account] account Vagrant Cloud account
# @param [String] org Organization name
# @param [String] box Box name
# @param [String] version Box version
# @yieldparam [VagrantCloud::Box::Version] version Requested Vagrant Cloud box version
# @yieldreturn [Integer]
# @return [Integer]
def with_version(account:, org:, box:, version:)
with_box(account: account, org: org, box: box) do |b|
v = b.versions.detect { |v| v.version == version }
if !v
@env.ui.error(I18n.t("cloud_command.version.not_found",
box_name: box, org: org, version: version))
return 1
end
yield v
end
end
# Load box version and yield
#
# @param [VagrantCloud::Account] account Vagrant Cloud account
# @param [String] org Organization name
# @param [String] box Box name
# @param [String] version Box version
# @param [String] provider Box version provider name
# @yieldparam [VagrantCloud::Box::Provider] provider Requested Vagrant Cloud box version provider
# @yieldreturn [Integer]
# @return [Integer]
def with_provider(account:, org:, box:, version:, provider:, architecture:)
with_version(account: account, org: org, box: box, version: version) do |v|
p = v.providers.detect { |p|
p.name == provider &&
p.architecture == architecture
}
if !p
@env.ui.error(I18n.t("cloud_command.provider.not_found",
org: org, box_name: box, version: version, provider_name: provider))
return 1
end
yield p
end
end
protected
# Extract box information for display
#
# @param [VagrantCloud::Box] box Box for extracting information
# @return [Hash<String,String>]
def box_info(box, options={})
current_version = box.current_version
if current_version
current_version = nil if !Array(options[:providers]).empty? &&
current_version.providers.none? { |p| options[:providers].include?(p.name) }
current_version = nil if !Array(options[:architectures]).empty? &&
current_version.providers.none? { |p| options[:architectures].include?(p.architecture) }
end
versions = box.versions
# Apply provider filter if defined
versions = versions.find_all { |v|
v.providers.any? { |p|
options[:providers].include?(p.name)
}
} if !Array(options[:providers]).empty?
# Apply architecture filter if defined
versions = versions.find_all { |v|
v.providers.any? { |p|
options[:architectures].include?(p.architecture)
}
} if !Array(options[:architectures]).empty?
raise "no matches" if current_version.nil? && versions.empty?
Hash.new.tap do |i|
i["Box"] = box.tag
i["Description"] = box.description
i["Private"] = box.private ? "yes" : "no"
i["Created"] = box.created_at
i["Updated"] = box.updated_at
if !current_version.nil?
i["Current Version"] = box.current_version.version
else
i["Current Version"] = "N/A"
end
i["Versions"] = versions.slice(0, 5).map(&:version).join(", ")
if box.versions.size > 5
i["Versions"] += " ..."
end
i["Downloads"] = format_downloads(box.downloads)
end
end
# Extract version information for display
#
# @param [VagrantCloud::Box::Version] version Box version for extracting information
# @return [Hash<String,String>]
def version_info(version)
provider_arches = version.providers.group_by(&:name).map { |provider_name, info|
"#{provider_name} (#{info.map(&:architecture).sort.join(", ")})"
}.sort.join("\n")
Hash.new.tap do |i|
i["Box"] = version.box.tag
i["Version"] = version.version
i["Description"] = version.description
i["Status"] = version.status
i["Providers"] = provider_arches
i["Created"] = version.created_at
i["Updated"] = version.updated_at
end
end
# Extract provider information for display
#
# @param [VagrantCloud::Box::Provider] provider Box provider for extracting information
# @return [Hash<String,String>]
def provider_info(provider)
{
"Box" => provider.version.box.tag,
"Private" => provider.version.box.private ? "yes" : "no",
"Version" => provider.version.version,
"Provider" => provider.name,
"Architecture" => provider.architecture,
"Default Architecture" => provider.default_architecture ? "yes" : "no",
}
end
# Print table results from search request
#
# @param [Vagrant::Environment] env Current Vagrant environment
# @param [Hash] column_labels A hash of key/value pairs for table labels (i.e. {col1: "COL1"})
# @param [Array] results An array of hashes representing search resuls
# @param [Array] to_jrust_keys - List of columns keys to right justify (left justify is defualt)
# @return [nil]
# @note Modified from https://stackoverflow.com/a/28685559
def print_search_table(env, column_labels, results, to_rjust_keys)
columns = column_labels.each_with_object({}) do |(col,label),h|
h[col] = {
label: label,
width: [results.map { |g| g[col].size }.max, label.size].max
}
end
write_header(env, columns)
write_divider(env, columns)
results.each { |h| write_line(env, columns, h, to_rjust_keys) }
write_divider(env, columns)
end
# Write the header for a table
#
# @param [Vagrant::Environment] env Current Vagrant environment
# @param [Array<Hash>] columns List of columns in Hash format with `:label` and `:width` keys
# @return [nil]
def write_header(env, columns)
env.ui.info "| #{ columns.map { |_,g| g[:label].ljust(g[:width]) }.join(' | ') } |"
nil
end
# Write a row divider for a table
#
# @param [Vagrant::Environment] env Current Vagrant environment
# @param [Array<Hash>] columns List of columns in Hash format with `:label` and `:width` keys
# @return [nil]
def write_divider(env, columns)
env.ui.info "+-#{ columns.map { |_,g| "-"*g[:width] }.join("-+-") }-+"
nil
end
# Write a line of content for a table
#
# @param [Vagrant::Environment] env Current Vagrant environment
# @param [Array<Hash>] columns List of columns in Hash format with `:label` and `:width` keys
# @param [Hash] h Values to print in row
# @param [Array<String>] to_rjust_keys List of columns to right justify
# @return [nil]
def write_line(env, columns, h, to_rjust_keys)
str = h.keys.map { |k|
if to_rjust_keys.include?(k)
h[k].rjust(columns[k][:width])
else
h[k].ljust(columns[k][:width])
end
}.join(" | ")
env.ui.info "| #{str} |"
nil
end
# Converts a string of numbers into a formatted number
#
# 1234 -> 1,234
#
# @param [String] number Numer to format
def format_downloads(number)
number.to_s.chars.reverse.each_slice(3).map(&:join).join(",").reverse
end
end
end
end