Merge pull request #10148 from briancain/add-cloud-command

Introduce `vagrant cloud` subcommand to Vagrant
This commit is contained in:
Brian Cain 2018-10-16 15:19:58 -07:00 committed by GitHub
commit 23de7f0898
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 4801 additions and 84 deletions

View File

@ -53,7 +53,7 @@ __vagrantinvestigate() {
_vagrant() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
commands="box connect destroy docker-exec docker-logs docker-run global-status halt help init list-commands login package plugin provision push rdp reload resume rsync rsync-auto share snapshot ssh ssh-config status suspend up version"
commands="box cloud connect destroy docker-exec docker-logs docker-run global-status halt help init list-commands login package plugin provision push rdp reload resume rsync rsync-auto share snapshot ssh ssh-config status suspend up version"
if [ $COMP_CWORD == 1 ]
then
@ -95,6 +95,11 @@ _vagrant() {
COMPREPLY=($(compgen -W "${box_commands}" -- ${cur}))
return 0
;;
"cloud")
cloud_commands="auth box search provider publish version"
COMPREPLY=($(compgen -W "${cloud_commands}" -- ${cur}))
return 0
;;
"plugin")
plugin_commands="install license list uninstall update"
COMPREPLY=($(compgen -W "${plugin_commands}" -- ${cur}))

View File

@ -49,7 +49,16 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
# Set the logging level on all "vagrant" namespaced
# logs as long as we have a valid level.
if level
logger = Log4r::Logger.new("vagrant")
# NOTE: We must do this little hack to allow
# rest-client to write using the `<<` operator.
# See https://github.com/rest-client/rest-client/issues/34#issuecomment-290858
# for more information
class VagrantLogger < Log4r::Logger
def << (msg)
debug(msg.strip)
end
end
logger = VagrantLogger.new("vagrant")
logger.outputters = Log4r::Outputter.stderr
logger.level = level
base_formatter = Log4r::BasicFormatter.new
@ -59,6 +68,11 @@ if ENV["VAGRANT_LOG"] && ENV["VAGRANT_LOG"] != ""
date_pattern: "%F %T"
)
end
# Vagrant Cloud gem uses RestClient to make HTTP requests, so
# log them if debug is enabled and use Vagrants logger
require 'rest_client'
RestClient.log = logger
Log4r::Outputter.stderr.formatter = Vagrant::Util::LoggingFormatter.new(base_formatter)
logger = nil
end

View File

@ -832,6 +832,14 @@ module Vagrant
error_key(:upload_source_missing)
end
class UploaderError < VagrantError
error_key(:uploader_error)
end
class UploaderInterrupted < UploaderError
error_key(:uploader_interrupted)
end
class VagrantInterrupt < VagrantError
error_key(:interrupted)
end

View File

@ -0,0 +1,96 @@
module Vagrant
module Util
class CurlHelper
# Hosts that do not require notification on redirect
SILENCED_HOSTS = [
"vagrantcloud.com".freeze,
"vagrantup.com".freeze
].freeze
def self.capture_output_proc(logger, ui, source=nil)
progress_data = ""
progress_regexp = /^\r\s*(\d.+?)\r/m
# Setup the proc that'll receive the real-time data from
# the downloader.
data_proc = Proc.new do |type, data|
# Type will always be "stderr" because that is the only
# type of data we're subscribed for notifications.
# Accumulate progress_data
progress_data << data
while true
# If the download has been redirected and we are no longer downloading
# from the original host, notify the user that the target host has
# changed from the source.
if progress_data.include?("Location")
location = progress_data.scan(/(^|[^\w-])Location: (.+?)$/m).flatten.compact.last.to_s.strip
if !location.empty?
location_uri = URI.parse(location)
unless location_uri.host.nil?
redirect_notify = false
logger.info("download redirected to #{location}")
source_uri = URI.parse(source)
source_host = source_uri.host.to_s.split(".", 2).last
location_host = location_uri.host.to_s.split(".", 2).last
if !redirect_notify && location_host != source_host && !SILENCED_HOSTS.include?(location_host)
ui.clear_line
ui.detail "Download redirected to host: #{location_uri.host}"
end
redirect_notify = true
end
end
progress_data.replace("")
break
end
# If we have a full amount of column data (two "\r") then
# we report new progress reports. Otherwise, just keep
# accumulating.
match = nil
check_match = true
while check_match
check_match = progress_regexp.match(progress_data)
if check_match
data = check_match[1].to_s
stop = progress_data.index(data) + data.length
progress_data.slice!(0, stop)
match = check_match
end
end
break if !match
# Ignore the first \r and split by whitespace to grab the columns
columns = data.strip.split(/\s+/)
# COLUMN DATA:
#
# 0 - % total
# 1 - Total size
# 2 - % received
# 3 - Received size
# 4 - % transferred
# 5 - Transferred size
# 6 - Average download speed
# 7 - Average upload speed
# 9 - Total time
# 9 - Time spent
# 10 - Time left
# 11 - Current speed
output = "Progress: #{columns[0]}% (Rate: #{columns[11]}/s, Estimated time remaining: #{columns[10]})"
ui.clear_line
ui.detail(output, new_line: false)
end
end
return data_proc
end
end
end
end

View File

@ -6,6 +6,7 @@ require "digest/sha1"
require "vagrant/util/busy"
require "vagrant/util/platform"
require "vagrant/util/subprocess"
require "vagrant/util/curl_helper"
module Vagrant
module Util
@ -88,85 +89,7 @@ module Vagrant
# tell us output so we can parse it out.
extra_subprocess_opts[:notify] = :stderr
progress_data = ""
progress_regexp = /^\r\s*(\d.+?)\r/m
# Setup the proc that'll receive the real-time data from
# the downloader.
data_proc = Proc.new do |type, data|
# Type will always be "stderr" because that is the only
# type of data we're subscribed for notifications.
# Accumulate progress_data
progress_data << data
while true
# If the download has been redirected and we are no longer downloading
# from the original host, notify the user that the target host has
# changed from the source.
if progress_data.include?("Location")
location = progress_data.scan(/(^|[^\w-])Location: (.+?)$/m).flatten.compact.last.to_s.strip
if !location.empty?
location_uri = URI.parse(location)
unless location_uri.host.nil?
@logger.info("download redirected to #{location}")
source_uri = URI.parse(source)
source_host = source_uri.host.to_s.split(".", 2).last
location_host = location_uri.host.to_s.split(".", 2).last
if !@redirect_notify && location_host != source_host && !SILENCED_HOSTS.include?(location_host)
@ui.clear_line
@ui.detail "Download redirected to host: #{location_uri.host}"
end
@redirect_notify = true
end
end
progress_data.replace("")
break
end
# If we have a full amount of column data (two "\r") then
# we report new progress reports. Otherwise, just keep
# accumulating.
match = nil
check_match = true
while check_match
check_match = progress_regexp.match(progress_data)
if check_match
data = check_match[1].to_s
stop = progress_data.index(data) + data.length
progress_data.slice!(0, stop)
match = check_match
end
end
break if !match
# Ignore the first \r and split by whitespace to grab the columns
columns = data.strip.split(/\s+/)
# COLUMN DATA:
#
# 0 - % total
# 1 - Total size
# 2 - % received
# 3 - Received size
# 4 - % transferred
# 5 - Transferred size
# 6 - Average download speed
# 7 - Average upload speed
# 9 - Total time
# 9 - Time spent
# 10 - Time left
# 11 - Current speed
output = "Progress: #{columns[0]}% (Rate: #{columns[11]}/s, Estimated time remaining: #{columns[10]})"
@ui.clear_line
@ui.detail(output, new_line: false)
end
end
data_proc = Vagrant::Util::CurlHelper.capture_output_proc(@logger, @ui, @source)
end
@logger.info("Downloader starting download: ")

View File

@ -0,0 +1,103 @@
require "uri"
require "log4r"
require "vagrant/util/busy"
require "vagrant/util/platform"
require "vagrant/util/subprocess"
require "vagrant/util/curl_helper"
module Vagrant
module Util
# This class uploads files using various protocols by subprocessing
# to cURL. cURL is a much more capable and complete download tool than
# a hand-rolled Ruby library, so we defer to its expertise.
class Uploader
# @param [String] destination - valid URL to upload file to
# @param [String] file - location of file to upload on disk
# @param [Hash] options
def initialize(destination, file, options=nil)
options ||= {}
@logger = Log4r::Logger.new("vagrant::util::uploader")
@destination = destination.to_s
@file = file.to_s
@ui = options[:ui]
@request_method = options[:method]
if !@request_method
@request_method = "PUT"
end
end
def upload!
data_proc = Vagrant::Util::CurlHelper.capture_output_proc(@logger, @ui)
@logger.info("Uploader starting upload: ")
@logger.info(" -- Source: #{@file}")
@logger.info(" -- Destination: #{@destination}")
options = build_options
subprocess_options = {notify: :stderr}
begin
execute_curl(options, subprocess_options, &data_proc)
rescue Errors::UploaderError => e
raise
ensure
@ui.clear_line if @ui
end
end
protected
def build_options
options = [@destination, "--request", @request_method, "--upload-file", @file]
return options
end
def execute_curl(options, subprocess_options, &data_proc)
options = options.dup
options << subprocess_options
# Create the callback that is called if we are interrupted
interrupted = false
int_callback = Proc.new do
@logger.info("Uploader interrupted!")
interrupted = true
end
# Execute!
result = Busy.busy(int_callback) do
Subprocess.execute("curl", *options, &data_proc)
end
# If the upload was interrupted, then raise a specific error
raise Errors::UploaderInterrupted if interrupted
# If it didn't exit successfully, we need to parse the data and
# show an error message.
if result.exit_code != 0
@logger.warn("Uploader exit code: #{result.exit_code}")
check = result.stderr.match(/\n*curl:\s+\((?<code>\d+)\)\s*(?<error>.*)$/)
if !check
err_msg = result.stderr
else
err_msg = check[:error]
end
raise Errors::UploaderError,
exit_code: result.exit_code,
message: err_msg
end
if @ui
@ui.clear_line
# Windows doesn't clear properly for some reason, so we just
# output one more newline.
@ui.detail("") if Platform.windows?
end
result
end
end
end
end

View File

@ -0,0 +1,90 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module AuthCommand
module Command
class Login < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud auth login [options]"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-c", "--check", "Checks if currently logged in") do |c|
options[:check] = c
end
o.on("-d", "--description DESCRIPTION", String, "Set description for the Vagrant Cloud token") do |d|
options[:description] = d
end
o.on("-k", "--logout", "Logout from Vagrant Cloud") do |k|
options[:logout] = k
end
o.on("-t", "--token TOKEN", String, "Set the Vagrant Cloud token") do |t|
options[:token] = t
end
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |l|
options[:login] = l
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
@client = Client.new(@env)
@client.username_or_email = options[:login]
# Determine what task we're actually taking based on flags
if options[:check]
return execute_check
elsif options[:logout]
return execute_logout
elsif options[:token]
return execute_token(options[:token])
else
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options)
end
0
end
def execute_check
if @client.logged_in?
@env.ui.success(I18n.t("cloud_command.check_logged_in"))
return 0
else
@env.ui.error(I18n.t("cloud_command.check_not_logged_in"))
return 1
end
end
def execute_logout
@client.clear_token
@env.ui.success(I18n.t("cloud_command.logged_out"))
return 0
end
def execute_token(token)
@client.store_token(token)
@env.ui.success(I18n.t("cloud_command.token_saved"))
if @client.logged_in?
@env.ui.success(I18n.t("cloud_command.check_logged_in"))
return 0
else
@env.ui.error(I18n.t("cloud_command.invalid_token"))
return 1
end
end
end
end
end
end
end

View File

@ -0,0 +1,42 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module AuthCommand
module Command
class Logout < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud auth logout [options]"
o.separator ""
o.separator "Log out of Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |l|
options[:login] = l
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if !argv.empty?
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
# Initializes client and deletes token on disk
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
@client.clear_token
@env.ui.success(I18n.t("cloud_command.logged_out"))
return 0
end
end
end
end
end
end

View File

@ -0,0 +1,20 @@
require "vagrant"
module VagrantPlugins
module CloudCommand
module AuthCommand
class Plugin < Vagrant.plugin("2")
name "vagrant cloud auth"
description <<-DESC
Authorization commands for Vagrant Cloud
DESC
command(:auth) do
require_relative "root"
Command::Root
end
end
end
end
end

View File

@ -0,0 +1,73 @@
module VagrantPlugins
module CloudCommand
module AuthCommand
module Command
class Root < Vagrant.plugin("2", :command)
def self.synopsis
"Manages Vagrant Cloud authorization related to Vagrant Cloud"
end
def initialize(argv, env)
super
@main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
@subcommands = Vagrant::Registry.new
@subcommands.register(:login) do
require File.expand_path("../login", __FILE__)
Command::Login
end
@subcommands.register(:logout) do
require File.expand_path("../logout", __FILE__)
Command::Logout
end
@subcommands.register(:whoami) do
require File.expand_path("../whoami", __FILE__)
Command::Whoami
end
end
def execute
if @main_args.include?("-h") || @main_args.include?("--help")
# Print the help for all the box commands.
return help
end
# If we reached this far then we must have a subcommand. If not,
# then we also just print the help and exit.
command_class = @subcommands.get(@sub_command.to_sym) if @sub_command
return help if !command_class || !@sub_command
@logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}")
# Initialize and execute the command class
command_class.new(@sub_args, @env).execute
end
# Prints the help out for this command
def help
opts = OptionParser.new do |opts|
opts.banner = "Usage: vagrant cloud auth <subcommand> [<args>]"
opts.separator ""
opts.separator "Authorization with Vagrant Cloud"
opts.separator ""
opts.separator "Available subcommands:"
# Add the available subcommands as separators in order to print them
# out as well.
keys = []
@subcommands.each { |key, value| keys << key.to_s }
keys.sort.each do |key|
opts.separator " #{key}"
end
opts.separator ""
opts.separator "For help on any individual subcommand run `vagrant cloud auth <subcommand> -h`"
end
@env.ui.info(opts.help, prefix: false)
end
end
end
end
end
end

View File

@ -0,0 +1,62 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module AuthCommand
module Command
class Whoami < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud auth whoami [options] [token]"
o.separator ""
o.separator "Display currently logged in user"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |l|
options[:login] = l
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.size > 1
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:login])
if argv.first
token = argv.first
else
token = @client.token
end
whoami(token, options[:username])
end
def whoami(access_token, username)
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(username, access_token, server_url)
begin
success = account.validate_token
user = success["user"]["username"]
@env.ui.success("Currently logged in as #{user}")
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.whoami.read_error", org: username))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,75 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module BoxCommand
module Command
class Create < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud box create [options] organization/box-name"
o.separator ""
o.separator "Creates an empty box entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-d", "--description DESCRIPTION", String, "Full description of the box") do |d|
options[:description] = d
end
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
o.on("-s", "--short-description DESCRIPTION", String, "Short description of the box") do |s|
options[:short] = s
end
o.on("-p", "--private", "Makes box private") do |p|
options[:private] = p
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 1
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
create_box(org, box_name, options, @client.token)
end
# @param [String] - org
# @param [String] - box_name
# @param [Hash] - options
def create_box(org, box_name, options, access_token)
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, options[:short], options[:description], access_token)
begin
success = box.create
@env.ui.success(I18n.t("cloud_command.box.create_success", org: org, box_name: box_name))
success = success.delete_if { |_, v| v.nil? }
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.box.create_fail", org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,65 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module BoxCommand
module Command
class Delete < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud box delete [options] organization/box-name"
o.separator ""
o.separator "Deletes box entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 1
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@env.ui.warn(I18n.t("cloud_command.box.delete_warn", box: argv.first))
cont = @env.ui.ask(I18n.t("cloud_command.continue"))
return 1 if cont.strip.downcase != "y"
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
delete_box(org, box_name, options[:username], @client.token)
end
def delete_box(org, box_name, username, access_token)
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(username, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
begin
success = box.delete(org, box_name)
@env.ui.success(I18n.t("cloud_command.box.delete_success", org: org, box_name: box_name))
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.box.delete_fail", org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,19 @@
require "vagrant"
module VagrantPlugins
module CloudCommand
module BoxCommand
class Plugin < Vagrant.plugin("2")
name "vagrant cloud box"
description <<-DESC
Box life cycle commands for Vagrant Cloud
DESC
command(:box) do
require_relative "root"
Command::Root
end
end
end
end
end

View File

@ -0,0 +1,77 @@
module VagrantPlugins
module CloudCommand
module BoxCommand
module Command
class Root < Vagrant.plugin("2", :command)
def self.synopsis
"Commands to manage boxes on Vagrant Cloud"
end
def initialize(argv, env)
super
@main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
@subcommands = Vagrant::Registry.new
@subcommands.register(:create) do
require File.expand_path("../create", __FILE__)
Command::Create
end
@subcommands.register(:delete) do
require File.expand_path("../delete", __FILE__)
Command::Delete
end
@subcommands.register(:show) do
require File.expand_path("../show", __FILE__)
Command::Show
end
@subcommands.register(:update) do
require File.expand_path("../update", __FILE__)
Command::Update
end
end
def execute
if @main_args.include?("-h") || @main_args.include?("--help")
# Print the help for all the box commands.
return help
end
# If we reached this far then we must have a subcommand. If not,
# then we also just print the help and exit.
command_class = @subcommands.get(@sub_command.to_sym) if @sub_command
return help if !command_class || !@sub_command
@logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}")
# Initialize and execute the command class
command_class.new(@sub_args, @env).execute
end
# Prints the help out for this command
def help
opts = OptionParser.new do |opts|
opts.banner = "Usage: vagrant cloud box <subcommand> [<args>]"
opts.separator ""
opts.separator "Commands to manage boxes on Vagrant Cloud"
opts.separator ""
opts.separator "Available subcommands:"
# Add the available subcommands as separators in order to print them
# out as well.
keys = []
@subcommands.each { |key, value| keys << key.to_s }
keys.sort.each do |key|
opts.separator " #{key}"
end
opts.separator ""
opts.separator "For help on any individual subcommand run `vagrant cloud box <subcommand> -h`"
end
@env.ui.info(opts.help, prefix: false)
end
end
end
end
end
end

View File

@ -0,0 +1,74 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module BoxCommand
module Command
class Show < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud box show [options] organization/box-name"
o.separator ""
o.separator "Displays a boxes attributes on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |u|
options[:username] = u
end
o.on("--versions VERSION", String, "Display box information for a specific version") do |v|
options[:version] = v
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 1
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
show_box(box[0], box[1], options, @client.token)
end
def show_box(org, box_name, options, access_token)
username = options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(username, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
begin
success = box.read(org, box_name)
if options[:version]
# show *this* version only
results = success["versions"].select{ |v| v if v["version"] == options[:version] }.first
if !results
@env.ui.warn(I18n.t("cloud_command.box.show_filter_empty", version: options[:version], org: org, box_name: box_name))
return 0
end
else
results = success
end
results = results.delete_if { |_, v| v.nil? }
VagrantPlugins::CloudCommand::Util.format_box_results(results, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.box.show_fail", org: org,box_name:box_name))
@env.ui.error(e)
return 1
end
end
end
end
end
end
end

View File

@ -0,0 +1,71 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module BoxCommand
module Command
class Update < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud box update [options] organization/box-name"
o.separator ""
o.separator "Updates a box entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-d", "--description DESCRIPTION", "Full description of the box") do |d|
options[:description] = d
end
o.on("-u", "--username", "The username of the organization that will own the box") do |u|
options[:username] = u
end
o.on("-s", "--short-description DESCRIPTION", "Short description of the box") do |s|
options[:short_description] = s
end
o.on("-p", "--private", "Makes box private") do |p|
options[:private] = p
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 1 || options.length == 0
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
update_box(box[0], box[1], options, @client.token)
end
def update_box(org, box_name, options, access_token)
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(options[:username], access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
options[:organization] = org
options[:name] = box_name
begin
success = box.update(options)
@env.ui.success(I18n.t("cloud_command.box.update_success", org: org, box_name: box_name))
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.box.update_fail", org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,264 @@
require "rest_client"
require "vagrant_cloud"
require "vagrant/util/downloader"
require "vagrant/util/presence"
require Vagrant.source_root.join("plugins/commands/cloud/errors")
module VagrantPlugins
module CloudCommand
class Client
######################################################################
# Class that deals with managing users 'local' token for Vagrant Cloud
######################################################################
APP = "app".freeze
include Vagrant::Util::Presence
attr_accessor :username_or_email
attr_accessor :password
attr_reader :two_factor_default_delivery_method
attr_reader :two_factor_delivery_methods
# Initializes a login client with the given Vagrant::Environment.
#
# @param [Vagrant::Environment] env
def initialize(env)
@logger = Log4r::Logger.new("vagrant::cloud::client")
@env = env
end
# Removes the token, effectively logging the user out.
def clear_token
@logger.info("Clearing token")
token_path.delete if token_path.file?
end
# Checks if the user is logged in by verifying their authentication
# token.
#
# @return [Boolean]
def logged_in?
token = self.token
return false if !token
Vagrant::Util::CredentialScrubber.sensitive(token)
with_error_handling do
url = "#{Vagrant.server_url}/api/v1/authenticate" +
"?access_token=#{token}"
RestClient.get(url, content_type: :json)
true
end
rescue Errors::Unauthorized
false
end
# Login logs a user in and returns the token for that user. The token
# is _not_ stored unless {#store_token} is called.
#
# @param [String] description
# @param [String] code
# @return [String] token The access token, or nil if auth failed.
def login(description: nil, code: nil)
@logger.info("Logging in '#{username_or_email}'")
Vagrant::Util::CredentialScrubber.sensitive(password)
response = post(
"/api/v1/authenticate", {
user: {
login: username_or_email,
password: password
},
token: {
description: description
},
two_factor: {
code: code
}
}
)
Vagrant::Util::CredentialScrubber.sensitive(response["token"])
response["token"]
end
# Requests a 2FA code
# @param [String] delivery_method
def request_code(delivery_method)
@env.ui.warn("Requesting 2FA code via #{delivery_method.upcase}...")
Vagrant::Util::CredentialScrubber.sensitive(password)
response = post(
"/api/v1/two-factor/request-code", {
user: {
login: username_or_email,
password: password
},
two_factor: {
delivery_method: delivery_method.downcase
}
}
)
two_factor = response['two_factor']
obfuscated_destination = two_factor['obfuscated_destination']
@env.ui.success("2FA code sent to #{obfuscated_destination}.")
end
# Issues a post to a Vagrant Cloud path with the given payload.
# @param [String] path
# @param [Hash] payload
# @return [Hash] response data
def post(path, payload)
with_error_handling do
url = File.join(Vagrant.server_url, path)
proxy = nil
proxy ||= ENV["HTTPS_PROXY"] || ENV["https_proxy"]
proxy ||= ENV["HTTP_PROXY"] || ENV["http_proxy"]
RestClient.proxy = proxy
response = RestClient::Request.execute(
method: :post,
url: url,
payload: JSON.dump(payload),
proxy: proxy,
headers: {
accept: :json,
content_type: :json,
user_agent: Vagrant::Util::Downloader::USER_AGENT,
},
)
JSON.load(response.to_s)
end
end
# Stores the given token locally, removing any previous tokens.
#
# @param [String] token
def store_token(token)
@logger.info("Storing token in #{token_path}")
token_path.open("w") do |f|
f.write(token)
end
nil
end
# Reads the access token if there is one. This will first read the
# `VAGRANT_CLOUD_TOKEN` environment variable and then fallback to the stored
# access token on disk.
#
# @return [String]
def token
if present?(ENV["VAGRANT_CLOUD_TOKEN"]) && token_path.exist?
@env.ui.warn <<-EOH.strip
Vagrant detected both the VAGRANT_CLOUD_TOKEN environment variable and a Vagrant login
token are present on this system. The VAGRANT_CLOUD_TOKEN environment variable takes
precedence over the locally stored token. To remove this error, either unset
the VAGRANT_CLOUD_TOKEN environment variable or remove the login token stored on disk:
~/.vagrant.d/data/vagrant_login_token
EOH
end
if present?(ENV["VAGRANT_CLOUD_TOKEN"])
@logger.debug("Using authentication token from environment variable")
return ENV["VAGRANT_CLOUD_TOKEN"]
end
if token_path.exist?
@logger.debug("Using authentication token from disk at #{token_path}")
return token_path.read.strip
end
if present?(ENV["ATLAS_TOKEN"])
@logger.warn("ATLAS_TOKEN detected within environment. Using ATLAS_TOKEN in place of VAGRANT_CLOUD_TOKEN.")
return ENV["ATLAS_TOKEN"]
end
@logger.debug("No authentication token in environment or #{token_path}")
nil
end
protected
def with_error_handling(&block)
yield
rescue RestClient::Unauthorized
@logger.debug("Unauthorized!")
raise Errors::Unauthorized
rescue RestClient::BadRequest => e
@logger.debug("Bad request:")
@logger.debug(e.message)
@logger.debug(e.backtrace.join("\n"))
parsed_response = JSON.parse(e.response)
errors = parsed_response["errors"].join("\n")
raise Errors::ServerError, errors: errors
rescue RestClient::NotAcceptable => e
@logger.debug("Got unacceptable response:")
@logger.debug(e.message)
@logger.debug(e.backtrace.join("\n"))
parsed_response = JSON.parse(e.response)
if two_factor = parsed_response['two_factor']
store_two_factor_information two_factor
if two_factor_default_delivery_method != APP
request_code two_factor_default_delivery_method
end
raise Errors::TwoFactorRequired
end
begin
errors = parsed_response["errors"].join("\n")
raise Errors::ServerError, errors: errors
rescue JSON::ParserError; end
@logger.debug("Got an unexpected error:")
@logger.debug(e.inspect)
raise Errors::Unexpected, error: e.inspect
rescue SocketError
@logger.info("Socket error")
raise Errors::ServerUnreachable, url: Vagrant.server_url.to_s
end
def token_path
@env.data_dir.join("vagrant_login_token")
end
def store_two_factor_information(two_factor)
@two_factor_default_delivery_method =
two_factor['default_delivery_method']
@two_factor_delivery_methods =
two_factor['delivery_methods']
@env.ui.warn "2FA is enabled for your account."
if two_factor_default_delivery_method == APP
@env.ui.info "Enter the code from your authenticator."
else
@env.ui.info "Default method is " \
"'#{two_factor_default_delivery_method}'."
end
other_delivery_methods =
two_factor_delivery_methods - [APP]
if other_delivery_methods.any?
other_delivery_methods_sentence = other_delivery_methods
.map { |word| "'#{word}'" }
.join(' or ')
@env.ui.info "You can also type #{other_delivery_methods_sentence} " \
"to request a new code."
end
end
end
end
end

View File

@ -0,0 +1,28 @@
module VagrantPlugins
module CloudCommand
module Errors
class Error < Vagrant::Errors::VagrantError
error_namespace("cloud_command.errors")
end
class ServerError < Error
error_key(:server_error)
end
class ServerUnreachable < Error
error_key(:server_unreachable)
end
class Unauthorized < Error
error_key(:unauthorized)
end
class Unexpected < Error
error_key(:unexpected_error)
end
class TwoFactorRequired < Error
end
end
end
end

View File

@ -0,0 +1,52 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module Command
class List < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud list [options] organization"
o.separator ""
o.separator "Search for boxes managed by a specific user/organization"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-j", "--json", "Formats results in JSON") do |j|
options[:check] = j
end
o.on("-l", "--limit", Integer, "Max number of search results (default is 25)") do |l|
options[:check] = l
end
o.on("-p", "--provider", "Comma separated list of providers to filter search. Defaults to all.") do |p|
options[:check] = p
end
o.on("-s", "--sort-by", "Column to sort list (created, downloads, updated)") do |s|
options[:check] = s
end
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.length > 1
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
# TODO: This endpoint is not implemented yet
0
end
end
end
end
end

View File

@ -0,0 +1,159 @@
en:
cloud_command:
publish:
update_continue: |-
%{obj} already exists, updating instead...
box_create:
Creating a box entry...
version_create:
Creating a version entry...
provider_create:
Creating a provider entry...
upload_provider:
Uploading provider with file %{file}
release:
Releasing box...
complete:
Complete! Published %{org}/%{box_name}
confirm:
warn: |-
You are about to publish a box on Vagrant Cloud with the following options:
box: |-
%{org}/%{box_name}: (v%{version}) for provider '%{provider_name}'
private: |-
Private: true
release: |-
Automatic Release: true
box_url: |-
Remote Box file: %{url}
box_description: |-
Box Description: %{description}
box_short_desc: |-
Box Short Description: %{short_description}
version_desc: |-
Version Description: %{version_description}
continue: |-
Do you wish to continue? [y/N]
box:
show_filter_empty: |-
No version matched %{version} for %{org}/%{box_name}
create_success: |-
Created box %{org}/%{box_name}
delete_success: |-
Deleted box %{org}/%{box_name}
delete_warn: |-
This will completely remove %{box} from Vagrant Cloud. This cannot be undone.
update_success: |-
Updated box %{org}/%{box_name}
search:
no_results: |-
No results found for `%{query}`
upload:
no_url: |-
No URL was provided to upload the provider
You will need to run the `vagrant cloud provider upload` command to provide a box
provider:
upload: |-
Uploading box file for '%{org}/%{box_name}' (v%{version}) for provider: '%{provider}'
upload_success: |-
Uploaded provider %{provider} on %{org}/%{box_name} for version %{version}
delete_warn: |-
This will completely remove provider %{provider} on version %{version} from %{box} on Vagrant Cloud. This cannot be undone.
create_success: |-
Created provider %{provider} on %{org}/%{box_name} for version %{version}
delete_success: |-
Deleted provider %{provider} on %{org}/%{box_name} for version %{version}
update_success: |-
Updated provider %{provider} on %{org}/%{box_name} for version %{version}
version:
create_success: |-
Created version %{version} on %{org}/%{box_name} for version %{version}
delete_success: |-
Deleted version %{version} on %{org}/%{box_name}
release_success: |-
Released version %{version} on %{org}/%{box_name}
revoke_success: |-
Revoked version %{version} on %{org}/%{box_name}
update_success: |-
Updated version %{version} on %{org}/%{box_name}
revoke_warn: |-
This will revoke version %{version} from %{box} from Vagrant Cloud. This cannot be undone.
release_warn: |-
This will release version %{version} from %{box} to Vagrant Cloud and be available to download.
delete_warn: |-
This will completely remove version %{version} from %{box} from Vagrant Cloud. This cannot be undone.
errors:
search:
fail: |-
Could not complete search request
publish:
fail: |-
Failed to create box %{org}/%{box_name}
box:
create_fail: |-
Failed to create box %{org}/%{box_name}
delete_fail: |-
Failed to delete box %{org}/%{box_name}
show_fail: |-
Could not get information about box %{org}/%{box_name}
update_fail: |-
Failed to update box %{org}/%{box_name}
whoami:
read_error: |-
Failed to read organization %{org}
provider:
create_fail: |-
Failed to create provider %{provider} on box %{org}/%{box_name} for version %{version}
update_fail: |-
Failed to update provider %{provider} on box %{org}/%{box_name} for version %{version}
delete_fail: |-
Failed to delete provider %{provider} on box %{org}/%{box_name} for version %{version}
upload_fail: |-
Failed to upload provider %{provider} on box %{org}/%{box_name} for version %{version}
version:
create_fail: |-
Failed to create version %{version} on box %{org}/%{box_name}
delete_fail: |-
Failed to delete version %{version} on box %{org}/%{box_name}
release_fail: |-
Failed to release version %{version} on box %{org}/%{box_name}
revoke_fail: |-
Failed to revoke version %{version} on box %{org}/%{box_name}
update_fail: |-
Failed to update version %{version} on box %{org}/%{box_name}
server_error: |-
The Vagrant Cloud server responded with a not-OK response:
%{errors}
server_unreachable: |-
The Vagrant Cloud server is not currently accepting connections. Please check
your network connection and try again later.
unauthorized: |-
Invalid username or password. Please try again.
unexpected_error: |-
An unexpected error occured: %{error}
check_logged_in: |-
You are already logged in.
check_not_logged_in: |-
You are not currently logged in. Please run `vagrant login` and provide
your login information to authenticate.
command_header: |-
In a moment we will ask for your username and password to HashiCorp's
Vagrant Cloud. After authenticating, we will store an access token locally on
disk. Your login details will be transmitted over a secure connection, and
are never stored on disk locally.
If you do not have an Vagrant Cloud account, sign up at
https://www.vagrantcloud.com
invalid_login: |-
Invalid username or password. Please try again.
invalid_token: |-
Invalid token. Please try again.
logged_in: |-
You are now logged in.
logged_out: |-
You are logged out.
token_saved: |-
The token was successfully saved.

View File

@ -0,0 +1,30 @@
require "vagrant"
require 'vagrant_cloud'
require Vagrant.source_root.join("plugins/commands/cloud/util")
require Vagrant.source_root.join("plugins/commands/cloud/client/client")
module VagrantPlugins
module CloudCommand
class Plugin < Vagrant.plugin("2")
name "vagrant-cloud"
description <<-DESC
Provides the cloud command and internal API access to Vagrant Cloud.
DESC
command(:cloud) do
require_relative "root"
init!
Command::Root
end
protected
def self.init!
return if defined?(@_init)
I18n.load_path << File.expand_path("../locales/en.yml", __FILE__)
I18n.reload!
@_init = true
end
end
end
end

View File

@ -0,0 +1,73 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module ProviderCommand
module Command
class Create < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud provider create [options] organization/box-name provider-name version [url]"
o.separator ""
o.separator "Creates a provider entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 4
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
provider_name = argv[1]
version = argv[2]
url = argv[3]
upload_provider(org, box_name, provider_name, version, url, @client.token, options)
end
def upload_provider(org, box_name, provider_name, version, url, access_token, options)
if !url
@env.ui.warn(I18n.t("cloud_command.upload.no_url"))
end
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token)
provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, url, org, box_name, access_token)
begin
success = provider.create_provider
@env.ui.success(I18n.t("cloud_command.provider.create_success", provider:provider_name, org: org, box_name: box_name, version: version))
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.provider.create_fail", provider:provider_name, org: org, box_name: box_name, version: version))
@env.ui.error(e)
return 1
end
end
end
end
end
end
end

View File

@ -0,0 +1,70 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module ProviderCommand
module Command
class Delete < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud provider delete [options] organization/box-name provider-name version"
o.separator ""
o.separator "Deletes a provider entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 3
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
provider_name = argv[1]
version = argv[2]
@env.ui.warn(I18n.t("cloud_command.provider.delete_warn", provider: provider_name, version:version, box: argv.first))
cont = @env.ui.ask(I18n.t("cloud_command.continue"))
return 1 if cont.strip.downcase != "y"
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
delete_provider(org, box_name, provider_name, version, @client.token, options)
end
def delete_provider(org, box_name, provider_name, version, access_token, options)
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token)
provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, nil, nil, nil, access_token)
begin
success = provider.delete
@env.ui.error(I18n.t("cloud_command.provider.delete_success", provider: provider_name, org: org, box_name: box_name, version: version))
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.provider.delete_fail", provider: provider_name, org: org, box_name: box_name, version: version))
@env.ui.error(e)
return 1
end
end
end
end
end
end
end

View File

@ -0,0 +1,19 @@
require "vagrant"
module VagrantPlugins
module CloudCommand
module ProviderCommand
class Plugin < Vagrant.plugin("2")
name "vagrant cloud box"
description <<-DESC
Provider life cycle commands for Vagrant Cloud
DESC
command(:provider) do
require_relative "root"
Command::Root
end
end
end
end
end

View File

@ -0,0 +1,77 @@
module VagrantPlugins
module CloudCommand
module ProviderCommand
module Command
class Root < Vagrant.plugin("2", :command)
def self.synopsis
"Provider commands"
end
def initialize(argv, env)
super
@main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
@subcommands = Vagrant::Registry.new
@subcommands.register(:create) do
require File.expand_path("../create", __FILE__)
Command::Create
end
@subcommands.register(:delete) do
require File.expand_path("../delete", __FILE__)
Command::Delete
end
@subcommands.register(:update) do
require File.expand_path("../update", __FILE__)
Command::Update
end
@subcommands.register(:upload) do
require File.expand_path("../upload", __FILE__)
Command::Upload
end
end
def execute
if @main_args.include?("-h") || @main_args.include?("--help")
# Print the help for all the provider commands.
return help
end
# If we reached this far then we must have a subcommand. If not,
# then we also just print the help and exit.
command_class = @subcommands.get(@sub_command.to_sym) if @sub_command
return help if !command_class || !@sub_command
@logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}")
# Initialize and execute the command class
command_class.new(@sub_args, @env).execute
end
# Prints the help out for this command
def help
opts = OptionParser.new do |opts|
opts.banner = "Usage: vagrant cloud provider <subcommand> [<args>]"
opts.separator ""
opts.separator "For various provider actions with Vagrant Cloud"
opts.separator ""
opts.separator "Available subcommands:"
# Add the available subcommands as separators in order to print them
# out as well.
keys = []
@subcommands.each { |key, value| keys << key.to_s }
keys.sort.each do |key|
opts.separator " #{key}"
end
opts.separator ""
opts.separator "For help on any individual subcommand run `vagrant cloud provider <subcommand> -h`"
end
@env.ui.info(opts.help, prefix: false)
end
end
end
end
end
end

View File

@ -0,0 +1,73 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module ProviderCommand
module Command
class Update < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud provider update [options] organization/box-name provider-name version url"
o.separator ""
o.separator "Updates a provider entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 4
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
provider_name = argv[1]
version = argv[2]
url = argv[3]
update_provider(org, box_name, provider_name, version, url, @client.token, options)
end
def update_provider(org, box_name, provider_name, version, url, access_token, options)
if !url
@env.ui.warn(I18n.t("cloud_command.upload.no_url"))
end
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token)
provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, url, org, box_name, access_token)
begin
success = provider.update
@env.ui.success(I18n.t("cloud_command.provider.update_success", provider:provider_name, org: org, box_name: box_name, version: version))
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.provider.update_fail", provider:provider_name, org: org, box_name: box_name, version: version))
@env.ui.error(e)
return 1
end
end
end
end
end
end
end

View File

@ -0,0 +1,75 @@
require 'optparse'
require "vagrant/util/uploader"
module VagrantPlugins
module CloudCommand
module ProviderCommand
module Command
class Upload < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud provider upload [options] organization/box-name provider-name version box-file"
o.separator ""
o.separator "Uploads a box file to Vagrant Cloud for a specific provider"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 4
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
provider_name = argv[1]
version = argv[2]
file = argv[3] # path expand
upload_provider(org, box_name, provider_name, version, file, @client.token, options)
end
def upload_provider(org, box_name, provider_name, version, file, access_token, options)
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
cloud_version = VagrantCloud::Version.new(box, version, nil, nil, access_token)
provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, nil, org, box_name, access_token)
ul = Vagrant::Util::Uploader.new(provider.upload_url, file, ui: @env.ui)
ui = Vagrant::UI::Prefixed.new(@env.ui, "cloud")
begin
ui.output(I18n.t("cloud_command.provider.upload", org: org, box_name: box_name, version: version, provider: provider_name))
ui.info("Upload File: #{file}")
ul.upload!
ui.success("Successfully uploaded box '#{org}/#{box_name}' (v#{version}) for '#{provider_name}'")
return 0
rescue Vagrant::Errors::UploaderError, VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.provider.upload_fail", provider: provider_name, org: org, box_name: box_name, version: version))
@env.ui.error(e)
return 1
end
end
end
end
end
end
end

View File

@ -0,0 +1,165 @@
require 'optparse'
require "vagrant/util/uploader"
module VagrantPlugins
module CloudCommand
module Command
class Publish < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud publish [options] organization/box-name version provider-name [provider-file]"
o.separator ""
o.separator "Create and release a new Vagrant Box on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("--box-version VERSION", String, "Version of box to create") do |v|
options[:box_version] = v
end
o.on("--url URL", String, "Remote URL to download this provider") do |u|
options[:url] = u
end
o.on("-d", "--description DESCRIPTION", String, "Full description of box") do |d|
options[:description] = d
end
o.on("--version-description DESCRIPTION", String, "Description of the version to create") do |v|
options[:version_description] = v
end
o.on("-f", "--force", "Disables confirmation to create or update box") do |f|
options[:force] = f
end
o.on("-p", "--private", "Makes box private") do |p|
options[:private] = p
end
o.on("-r", "--release", "Releases box") do |p|
options[:release] = p
end
o.on("-s", "--short-description DESCRIPTION", String, "Short description of the box") do |s|
options[:short_description] = s
end
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 4 || argv.length < 3
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
version = argv[1]
provider_name = argv[2]
box_file = argv[3] # path expand
publish_box(org, box_name, version, provider_name, box_file, options, @client.token)
end
def publish_box(org, box_name, version, provider_name, box_file, options, access_token)
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
@env.ui.warn(I18n.t("cloud_command.publish.confirm.warn"))
@env.ui.info(I18n.t("cloud_command.publish.confirm.box", org: org,
box_name: box_name, version: version, provider_name: provider_name))
@env.ui.info(I18n.t("cloud_command.publish.confirm.private")) if options[:private]
@env.ui.info(I18n.t("cloud_command.publish.confirm.release")) if options[:release]
@env.ui.info(I18n.t("cloud_command.publish.confirm.box_url",
url: options[:url])) if options[:url]
@env.ui.info(I18n.t("cloud_command.publish.confirm.box_description",
description: options[:description])) if options[:description]
@env.ui.info(I18n.t("cloud_command.publish.confirm.box_short_desc",
short_description: options[:short_description])) if options[:short_description]
@env.ui.info(I18n.t("cloud_command.publish.confirm.version_desc",
version_description: options[:version_description])) if options[:version_description]
if !options[:force]
cont = @env.ui.ask(I18n.t("cloud_command.continue"))
return 1 if cont.strip.downcase != "y"
end
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, options[:short_description], options[:description], access_token)
cloud_version = VagrantCloud::Version.new(box, version, nil, options[:version_description], access_token)
provider = VagrantCloud::Provider.new(cloud_version, provider_name, nil, options[:url], org, box_name, access_token)
ui = Vagrant::UI::Prefixed.new(@env.ui, "cloud")
begin
ui.info(I18n.t("cloud_command.publish.box_create"))
box.create
rescue VagrantCloud::ClientError => e
if e.error_code == 422
ui.warn(I18n.t("cloud_command.publish.update_continue", obj: "Box"))
box.update(options)
else
@env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
end
begin
ui.info(I18n.t("cloud_command.publish.version_create"))
cloud_version.create_version
rescue VagrantCloud::ClientError => e
if e.error_code == 422
ui.warn(I18n.t("cloud_command.publish.update_continue", obj: "Version"))
cloud_version.update
else
@env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
end
begin
ui.info(I18n.t("cloud_command.publish.provider_create"))
provider.create_provider
rescue VagrantCloud::ClientError => e
if e.error_code == 422
ui.warn(I18n.t("cloud_command.publish.update_continue", obj: "Provider"))
provider.update
else
@env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
end
begin
if !options[:url]
box_file = File.absolute_path(box_file)
ui.info(I18n.t("cloud_command.publish.upload_provider", file: box_file))
ul = Vagrant::Util::Uploader.new(provider.upload_url, box_file, ui: @env.ui)
ul.upload!
end
if options[:release]
ui.info(I18n.t("cloud_command.publish.release"))
cloud_version.release
end
@env.ui.success(I18n.t("cloud_command.publish.complete", org: org, box_name: box_name))
success = box.read(org, box_name)
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue Vagrant::Errors::UploaderError, VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.publish.fail", org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end

View File

@ -0,0 +1,104 @@
module VagrantPlugins
module CloudCommand
module Command
class Root < Vagrant.plugin("2", :command)
def self.synopsis
"manages everything related to Vagrant Cloud"
end
def initialize(argv, env)
super
@main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
@subcommands = Vagrant::Registry.new
@subcommand_helptext = {}
@subcommands.register(:auth) do
require File.expand_path("../auth/root", __FILE__)
AuthCommand::Command::Root
end
@subcommand_helptext[:auth] = "For various authorization operations on Vagrant Cloud"
@subcommands.register(:box) do
require File.expand_path("../box/root", __FILE__)
BoxCommand::Command::Root
end
@subcommand_helptext[:box] = "For managing a Vagrant box entry on Vagrant Cloud"
# TODO: Uncomment this when API endpoint exists
#@subcommands.register(:list) do
# require File.expand_path("../list", __FILE__)
# List
#end
#@subcommand_helptext[:list] = "Displays a list of Vagrant boxes that the current user manages"
@subcommands.register(:search) do
require File.expand_path("../search", __FILE__)
Search
end
@subcommand_helptext[:search] = "Search Vagrant Cloud for available boxes"
@subcommands.register(:provider) do
require File.expand_path("../provider/root", __FILE__)
ProviderCommand::Command::Root
end
@subcommand_helptext[:provider] = "For managing a Vagrant box's provider options"
@subcommands.register(:publish) do
require File.expand_path("../publish", __FILE__)
Publish
end
@subcommand_helptext[:publish] = "A complete solution for creating or updating a new box on Vagrant Cloud"
@subcommands.register(:version) do
require File.expand_path("../version/root", __FILE__)
VersionCommand::Command::Root
end
@subcommand_helptext[:version] = "For managing a Vagrant box's versions"
end
def execute
if @main_args.include?("-h") || @main_args.include?("--help")
# Print the help for all the box commands.
return help
end
# If we reached this far then we must have a subcommand. If not,
# then we also just print the help and exit.
command_class = @subcommands.get(@sub_command.to_sym) if @sub_command
return help if !command_class || !@sub_command
@logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}")
# Initialize and execute the command class
command_class.new(@sub_args, @env).execute
end
# Prints the help out for this command
def help
opts = OptionParser.new do |opts|
opts.banner = "Usage: vagrant cloud <subcommand> [<args>]"
opts.separator ""
opts.separator "The cloud command can be used for taking actions against"
opts.separator "Vagrant Cloud like searching or uploading a Vagrant Box"
opts.separator ""
opts.separator "Available subcommands:"
# Add the available subcommands as separators in order to print them
# out as well.
keys = []
@subcommands.each { |key, value| keys << key.to_s }
keys.sort.each do |key|
opts.separator " #{key.ljust(15)} #{@subcommand_helptext[key.to_sym]}"
end
opts.separator ""
opts.separator "For help on any individual subcommand run `vagrant cloud <subcommand> -h`"
end
@env.ui.info(opts.help, prefix: false)
end
end
end
end
end

View File

@ -0,0 +1,83 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module Command
class Search < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud search [options] query"
o.separator ""
o.separator "Search for boxes managed by a specific"
o.separator "user/organization on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-j", "--json", "Formats results in JSON") do |j|
options[:json] = j
end
o.on("-p", "--page PAGE", Integer, "The page to display Default: 1") do |j|
options[:page] = j
end
o.on("-s", "--short", "Shows a simple list of box names") do |s|
options[:short] = s
end
o.on("-o", "--order ORDER", String, "Order to display results ('desc' or 'asc') Default: 'desc'") do |o|
options[:order] = o
end
o.on("-l", "--limit LIMIT", Integer, "Max number of search results Default: 25") do |l|
options[:limit] = l
end
o.on("-p", "--provider PROVIDER", String, "Filter search results to a single provider. Defaults to all.") do |p|
options[:provider] = p
end
o.on("--sort-by SORT", "Field to sort results on (created, downloads, updated) Default: downloads") do |s|
options[:sort] = s
end
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.length > 1
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
query = argv.first
options[:limit] = 25 if !(options[:limit].to_i < 1) && !options[:limit]
search(query, options, @client.token)
end
def search(query, options, access_token)
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
search = VagrantCloud::Search.new(access_token, server_url)
begin
search_results = search.search(query, options[:provider], options[:sort], options[:order], options[:limit], options[:page])
if !search_results["boxes"].empty?
VagrantPlugins::CloudCommand::Util.format_search_results(search_results["boxes"], options[:short], options[:json], @env)
else
@env.ui.warn(I18n.t("cloud_command.search.no_results", query: query))
end
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.search.fail"))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end

View File

@ -0,0 +1,206 @@
module VagrantPlugins
module CloudCommand
class Util
class << self
# @param [String] username - Vagrant Cloud username
# @param [String] access_token - Vagrant Cloud Token used to authenticate
# @param [String] vagrant_cloud_server - Vagrant Cloud server to make API request
# @return [VagrantCloud::Account]
def account(username, access_token, vagrant_cloud_server)
if !defined?(@_account)
@_account = VagrantCloud::Account.new(username, access_token, vagrant_cloud_server)
end
@_account
end
def api_server_url
if Vagrant.server_url == Vagrant::DEFAULT_SERVER_URL
return "#{Vagrant.server_url}/api/v1"
else
return Vagrant.server_url
end
end
# @param [Vagrant::Environment] env
# @param [Hash] options
# @returns [VagrantPlugins::CloudCommand::Client]
def client_login(env, options)
if !defined?(@_client)
@_client = Client.new(env)
return @_client if @_client.logged_in?
# 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
@_client
end
# ===================================================
# Modified from https://stackoverflow.com/a/28685559
# for printing arrays of hashes in formatted tables
# ===================================================
# @param [Vagrant::Environment] - env
# @param [Hash] - column_labels - A hash of key values for table labels (i.e. {:col1=>"COL1", :col2=>"COL2"})
# @param [Array] - results - An array of hashes
# @param [Array] - to_jrust_keys - An array of column keys that should be right justified (default is left justified for all columns)
def print_search_table(env, column_labels, results, to_rjust_keys)
columns = column_labels.each_with_object({}) { |(col,label),h|
h[col] = { label: label,
width: [results.map { |g| g[col].size }.max, label.size].max
}}
write_header(env, columns)
write_divider(env, columns)
results.each { |h| write_line(env, columns, h,to_rjust_keys) }
write_divider(env, columns)
end
def write_header(env, columns)
env.ui.info "| #{ columns.map { |_,g| g[:label].ljust(g[:width]) }.join(' | ') } |"
end
def write_divider(env, columns)
env.ui.info "+-#{ columns.map { |_,g| "-"*g[:width] }.join("-+-") }-+"
end
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} |"
end
# ===================================================
# ===================================================
# Takes a "mostly" flat key=>value hash from Vagrant Cloud
# and prints its results in a list
#
# @param [Hash] - results - A response hash from vagrant cloud
# @param [Vagrant::Environment] - env
def format_box_results(results, env)
# TODO: remove other description fields? Maybe leave "short"?
results.delete("description_html")
if results["current_version"]
versions = results.delete("versions")
results["providers"] = results["current_version"]["providers"]
results["old_versions"] = versions.map{ |v| v["version"] }[1..5].join(", ") + "..."
end
width = results.keys.map{|k| k.size}.max
results.each do |k,v|
if k == "versions"
v = v.map{ |ver| ver["version"] }.join(", ")
elsif k == "current_version"
v = v["version"]
elsif k == "providers"
v = v.map{ |p| p["name"] }.join(", ")
elsif k == "downloads"
v = format_downloads(v.to_s)
end
whitespace = width-k.size
env.ui.info "#{k}:" + "".ljust(whitespace) + " #{v}"
end
end
# Converts a string of numbers into a formatted number
#
# 1234 -> 1,234
#
# @param [String] - download_string
def format_downloads(download_string)
return download_string.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end
# @param [Array] search_results - Box search results from Vagrant Cloud
# @param [String,nil] short - determines if short version will be printed
# @param [String,nil] json - determines if json version will be printed
# @param [Vagrant::Environment] - env
def format_search_results(search_results, short, json, env)
result = []
search_results.each do |b|
box = {}
box = {
name: b["tag"],
version: b["current_version"]["version"],
downloads: format_downloads(b["downloads"].to_s),
providers: b["current_version"]["providers"].map{ |p| p["name"] }.join(",")
}
result << box
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
end
end
end
end
end

View File

@ -0,0 +1,69 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module VersionCommand
module Command
class Create < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud version create [options] organization/box-name version"
o.separator ""
o.separator "Creates a version entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-d", "--description DESCRIPTION", String, "A description for this version") do |d|
options[:description] = d
end
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 2
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
version = argv[1]
create_version(org, box_name, version, @client.token, options)
end
def create_version(org, box_name, box_version, access_token, options)
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
version = VagrantCloud::Version.new(box, box_version, nil, options[:description], access_token)
begin
success = version.create_version
@env.ui.success(I18n.t("cloud_command.version.create_success", version: box_version, org: org, box_name: box_name))
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.version.create_fail", version: box_version, org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,68 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module VersionCommand
module Command
class Delete < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud version delete [options] organization/box-name version"
o.separator ""
o.separator "Deletes a version entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 2
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
version = argv[1]
@env.ui.warn(I18n.t("cloud_command.version.delete_warn", version: version, box: argv.first))
cont = @env.ui.ask(I18n.t("cloud_command.continue"))
return 1 if cont.strip.downcase != "y"
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
delete_version(org, box_name, version, options, @client.token)
end
def delete_version(org, box_name, box_version, options, access_token)
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
version = VagrantCloud::Version.new(box, box_version, nil, nil, access_token)
begin
success = version.delete
@env.ui.success(I18n.t("cloud_command.version.delete_success", version: box_version, org: org, box_name: box_name))
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.version.delete_fail", version: box_version, org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,19 @@
require "vagrant"
module VagrantPlugins
module CloudCommand
module VersionCommand
class Plugin < Vagrant.plugin("2")
name "vagrant cloud version"
description <<-DESC
Version life cycle commands for Vagrant Cloud
DESC
command(:version) do
require_relative "root"
Command::Root
end
end
end
end
end

View File

@ -0,0 +1,69 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module VersionCommand
module Command
class Release < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud version release [options] organization/box-name version"
o.separator ""
o.separator "Releases a version entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 2
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@env.ui.warn(I18n.t("cloud_command.version.release_warn", version: argv[1], box: argv.first))
cont = @env.ui.ask(I18n.t("cloud_command.continue"))
return 1 if cont.strip.downcase != "y"
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
version = argv[1]
release_version(org, box_name, version, @client.token, options)
end
def release_version(org, box_name, version, access_token, options)
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
version = VagrantCloud::Version.new(box, version, nil, nil, access_token)
begin
success = version.release
@env.ui.success(I18n.t("cloud_command.version.release_success", version: version, org: org, box_name: box_name))
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.version.release_fail", version: version, org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,69 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module VersionCommand
module Command
class Revoke < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud version revoke [options] organization/box-name version"
o.separator ""
o.separator "Revokes a version entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 2
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@env.ui.warn(I18n.t("cloud_command.version.revoke_warn", version: argv[1], box: argv.first))
cont = @env.ui.ask(I18n.t("cloud_command.continue"))
return 1 if cont.strip.downcase != "y"
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
version = argv[1]
revoke_version(org, box_name, version, @client.token, options)
end
def revoke_version(org, box_name, box_version, access_token, options)
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
version = VagrantCloud::Version.new(box, box_version, nil, nil, access_token)
begin
success = version.revoke
@env.ui.success(I18n.t("cloud_command.version.revoke_success", version: box_version, org: org, box_name: box_name))
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.version.revoke_fail", version: box_version, org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -0,0 +1,81 @@
module VagrantPlugins
module CloudCommand
module VersionCommand
module Command
class Root < Vagrant.plugin("2", :command)
def self.synopsis
"Version commands"
end
def initialize(argv, env)
super
@main_args, @sub_command, @sub_args = split_main_and_subcommand(argv)
@subcommands = Vagrant::Registry.new
@subcommands.register(:create) do
require File.expand_path("../create", __FILE__)
Command::Create
end
@subcommands.register(:delete) do
require File.expand_path("../delete", __FILE__)
Command::Delete
end
@subcommands.register(:revoke) do
require File.expand_path("../revoke", __FILE__)
Command::Revoke
end
@subcommands.register(:release) do
require File.expand_path("../release", __FILE__)
Command::Release
end
@subcommands.register(:update) do
require File.expand_path("../update", __FILE__)
Command::Update
end
end
def execute
if @main_args.include?("-h") || @main_args.include?("--help")
# Print the help for all the version commands.
return help
end
# If we reached this far then we must have a subcommand. If not,
# then we also just print the help and exit.
command_class = @subcommands.get(@sub_command.to_sym) if @sub_command
return help if !command_class || !@sub_command
@logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}")
# Initialize and execute the command class
command_class.new(@sub_args, @env).execute
end
# Prints the help out for this command
def help
opts = OptionParser.new do |opts|
opts.banner = "Usage: vagrant cloud version <subcommand> [<args>]"
opts.separator ""
opts.separator "For taking various actions against a Vagrant box's version attribute on Vagrant Cloud"
opts.separator ""
opts.separator "Available subcommands:"
# Add the available subcommands as separators in order to print them
# out as well.
keys = []
@subcommands.each { |key, value| keys << key.to_s }
keys.sort.each do |key|
opts.separator " #{key}"
end
opts.separator ""
opts.separator "For help on any individual subcommand run `vagrant cloud version <subcommand> -h`"
end
@env.ui.info(opts.help, prefix: false)
end
end
end
end
end
end

View File

@ -0,0 +1,69 @@
require 'optparse'
module VagrantPlugins
module CloudCommand
module VersionCommand
module Command
class Update < Vagrant.plugin("2", :command)
def execute
options = {}
opts = OptionParser.new do |o|
o.banner = "Usage: vagrant cloud version update [options] organization/box-name version"
o.separator ""
o.separator "Updates a version entry on Vagrant Cloud"
o.separator ""
o.separator "Options:"
o.separator ""
o.on("-d", "--description DESCRIPTION", "A description for this version") do |d|
options[:description] = d
end
o.on("-u", "--username USERNAME_OR_EMAIL", String, "Vagrant Cloud username or email address") do |t|
options[:username] = u
end
end
# Parse the options
argv = parse_options(opts)
return if !argv
if argv.empty? || argv.length > 2
raise Vagrant::Errors::CLIInvalidUsage,
help: opts.help.chomp
end
@client = VagrantPlugins::CloudCommand::Util.client_login(@env, options[:username])
box = argv.first.split('/', 2)
org = box[0]
box_name = box[1]
version = argv[1]
update_version(org, box_name, version, @client.token, options)
end
def update_version(org, box_name, box_version, access_token, options)
org = options[:username] if options[:username]
server_url = VagrantPlugins::CloudCommand::Util.api_server_url
account = VagrantPlugins::CloudCommand::Util.account(org, access_token, server_url)
box = VagrantCloud::Box.new(account, box_name, nil, nil, nil, access_token)
version = VagrantCloud::Version.new(box, box_version, nil, options[:description], access_token)
begin
success = version.update
@env.ui.success(I18n.t("cloud_command.version.update_success", version: box_version, org: org, box_name: box_name))
success = success.delete_if{|_, v|v.nil?}
VagrantPlugins::CloudCommand::Util.format_box_results(success, @env)
return 0
rescue VagrantCloud::ClientError => e
@env.ui.error(I18n.t("cloud_command.errors.version.update_fail", version: box_version, org: org, box_name: box_name))
@env.ui.error(e)
return 1
end
return 1
end
end
end
end
end
end

View File

@ -12,9 +12,10 @@ module VagrantPlugins
DESC
command(:login) do
require_relative "command"
require File.expand_path("../../cloud/auth/login", __FILE__)
init!
Command
$stderr.puts "WARNING: This command has been deprecated in favor of `vagrant cloud auth login`"
VagrantPlugins::CloudCommand::AuthCommand::Command::Login
end
action_hook(:cloud_authenticated_boxes, :authenticate_box_url) do |hook|
@ -26,7 +27,7 @@ module VagrantPlugins
def self.init!
return if defined?(@_init)
I18n.load_path << File.expand_path("../locales/en.yml", __FILE__)
I18n.load_path << File.expand_path("../../cloud/locales/en.yml", __FILE__)
I18n.reload!
@_init = true
end

View File

@ -1509,6 +1509,16 @@ en:
the source location for upload an try again.
Source Path: %{source}
uploader_error: |-
An error occurred while uploading the file. The error
message, if any, is reproduced below. Please fix this error and try
again.
exit code: %{exit_code}
%{message}
uploader_interrupted: |-
The upload was interrupted by an external signal. It did not
complete.
vagrantfile_exists: |-
`Vagrantfile` already exists in this directory. Remove it before
running `vagrant init`.

View File

@ -0,0 +1,103 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/auth/login")
describe VagrantPlugins::CloudCommand::AuthCommand::Command::Login do
include_context "unit"
let(:argv) { [] }
let(:env) { isolated_environment.create_vagrant_env }
let(:token_path) { env.data_dir.join("vagrant_login_token") }
let(:stdout) { StringIO.new }
let(:stderr) { StringIO.new }
subject { described_class.new(argv, env) }
before do
stub_env("ATLAS_TOKEN" => "")
end
let(:action_runner) { double("action_runner") }
before do
allow(env).to receive(:action_runner).and_return(action_runner)
end
describe "#execute" do
context "with no args" do
let(:argv) { [] }
end
context "with --check" do
let(:argv) { ["--check"] }
context "when there is a token" do
before do
stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate})
.to_return(status: 200)
end
before do
File.open(token_path, "w+") { |f| f.write("abcd1234") }
end
it "returns 0" do
expect(subject.execute).to eq(0)
end
end
context "when there is no token" do
it "returns 1" do
expect(subject.execute).to eq(1)
end
end
end
context "with --logout" do
let(:argv) { ["--logout"] }
it "returns 0" do
expect(subject.execute).to eq(0)
end
it "clears the token" do
subject.execute
expect(File.exist?(token_path)).to be(false)
end
end
context "with --token" do
let(:argv) { ["--token", "efgh5678"] }
context "when the token is valid" do
before do
stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate})
.to_return(status: 200)
end
it "sets the token" do
subject.execute
token = File.read(token_path).strip
expect(token).to eq("efgh5678")
end
it "returns 0" do
expect(subject.execute).to eq(0)
end
end
context "when the token is invalid" do
before do
stub_request(:get, %r{^#{Vagrant.server_url}/api/v1/authenticate})
.to_return(status: 401)
end
it "returns 1" do
expect(subject.execute).to eq(1)
end
end
end
end
end

View File

@ -0,0 +1,43 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/auth/logout")
describe VagrantPlugins::CloudCommand::AuthCommand::Command::Logout do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
end
context "with any arguments" do
let (:argv) { ["stuff", "things"] }
it "shows the help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with no arguments" do
it "logs you out" do
expect(client).to receive(:clear_token)
expect(subject.execute).to eq(0)
end
end
end

View File

@ -0,0 +1,54 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/auth/whoami")
describe VagrantPlugins::CloudCommand::AuthCommand::Command::Whoami do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:account) { double("account") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:account).
and_return(account)
end
context "with too many arguments" do
let(:argv) { ["token", "token", "token"] }
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with username" do
let(:argv) { ["token"] }
let(:org_hash) { {"user"=>{"username"=>"mario"}, "boxes"=>[{"name"=>"box"}]} }
it "gets information about a user" do
expect(account).to receive(:validate_token).and_return(org_hash)
expect(subject.execute).to eq(0)
end
it "returns 1 if encountering an error making request" do
allow(account).to receive(:validate_token).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,61 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/box/create")
describe VagrantPlugins::CloudCommand::BoxCommand::Command::Create do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "-s", "short", "-d", "long"] }
it "creates a box" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, "short", "long", client.token)
.and_return(box)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(box).to receive(:create).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, "short", "long", client.token)
.and_return(box)
allow(box).to receive(:create).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 422))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,62 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/box/delete")
describe VagrantPlugins::CloudCommand::BoxCommand::Command::Delete do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(iso_env.ui).to receive(:ask).
and_return("y")
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name"] }
it "creates a box" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
expect(box).to receive(:delete).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(box).to receive(:delete).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,63 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/box/show")
describe VagrantPlugins::CloudCommand::BoxCommand::Command::Show do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(iso_env.ui).to receive(:ask).
and_return("y")
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name"] }
it "creates a box" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
expect(box).to receive(:read).and_return({})
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(box).to receive(:read).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,64 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/box/update")
describe VagrantPlugins::CloudCommand::BoxCommand::Command::Update do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "-d", "update", "-s", "short"] }
it "creates a box" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
expect(box).to receive(:update).
with(organization: "vagrant", name: "box-name", description: "update", short_description: "short").
and_return({})
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(box).to receive(:update).
with(organization: "vagrant", name: "box-name", description: "update", short_description: "short").
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,261 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/client/client")
describe VagrantPlugins::CloudCommand::Client do
include_context "unit"
let(:env) { isolated_environment.create_vagrant_env }
subject(:client) { described_class.new(env) }
before(:all) do
I18n.load_path << Vagrant.source_root.join("plugins/commands/cloud/locales/en.yml")
I18n.reload!
end
before do
stub_env("ATLAS_TOKEN" => nil)
subject.clear_token
end
describe "#logged_in?" do
let(:url) { "#{Vagrant.server_url}/api/v1/authenticate?access_token=#{token}" }
let(:headers) { { "Content-Type" => "application/json" } }
before { allow(subject).to receive(:token).and_return(token) }
context "when there is no token" do
let(:token) { nil }
it "returns false" do
expect(subject.logged_in?).to be(false)
end
end
context "when there is a token" do
let(:token) { "ABCD1234" }
it "returns true if the endpoint returns a 200" do
stub_request(:get, url)
.with(headers: headers)
.to_return(body: JSON.pretty_generate("token" => token))
expect(subject.logged_in?).to be(true)
end
it "raises an error if the endpoint returns a non-200" do
stub_request(:get, url)
.with(headers: headers)
.to_return(body: JSON.pretty_generate("bad" => true), status: 401)
expect(subject.logged_in?).to be(false)
end
it "raises an exception if the server cannot be found" do
stub_request(:get, url)
.to_raise(SocketError)
expect { subject.logged_in? }
.to raise_error(VagrantPlugins::CloudCommand::Errors::ServerUnreachable)
end
end
end
describe "#login" do
let(:request) {
{
user: {
login: login,
password: password,
},
token: {
description: description,
},
two_factor: {
code: nil
}
}
}
let(:login) { "foo" }
let(:password) { "supersecretpassword" }
let(:description) { "Token description" }
let(:headers) {
{
"Accept" => "application/json",
"Content-Type" => "application/json",
}
}
let(:response) {
{
token: "mysecrettoken"
}
}
it "returns the access token after successful login" do
stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate").
with(body: JSON.dump(request), headers: headers).
to_return(status: 200, body: JSON.dump(response))
client.username_or_email = login
client.password = password
expect(client.login(description: "Token description")).to eq("mysecrettoken")
end
context "when 2fa is required" do
let(:response) {
{
two_factor: {
default_delivery_method: default_delivery_method,
delivery_methods: delivery_methods
}
}
}
let(:default_delivery_method) { "app" }
let(:delivery_methods) { ["app"] }
before do
stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate").
to_return(status: 406, body: JSON.dump(response))
end
it "raises a two-factor required error" do
expect {
client.login
}.to raise_error(VagrantPlugins::CloudCommand::Errors::TwoFactorRequired)
end
context "when the default delivery method is not app" do
let(:default_delivery_method) { "sms" }
let(:delivery_methods) { ["app", "sms"] }
it "requests a code and then raises a two-factor required error" do
expect(client)
.to receive(:request_code)
.with(default_delivery_method)
expect {
client.login
}.to raise_error(VagrantPlugins::CloudCommand::Errors::TwoFactorRequired)
end
end
end
context "on bad login" do
before do
stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate").
to_return(status: 401, body: "")
end
it "raises an error" do
expect {
client.login
}.to raise_error(VagrantPlugins::CloudCommand::Errors::Unauthorized)
end
end
context "if it can't reach the server" do
before do
stub_request(:post, "#{Vagrant.server_url}/api/v1/authenticate").
to_raise(SocketError)
end
it "raises an exception" do
expect {
subject.login
}.to raise_error(VagrantPlugins::CloudCommand::Errors::ServerUnreachable)
end
end
end
describe "#request_code" do
let(:request) {
{
user: {
login: login,
password: password,
},
two_factor: {
delivery_method: delivery_method
}
}
}
let(:login) { "foo" }
let(:password) { "supersecretpassword" }
let(:delivery_method) { "sms" }
let(:headers) {
{
"Accept" => "application/json",
"Content-Type" => "application/json"
}
}
let(:response) {
{
two_factor: {
obfuscated_destination: "SMS number ending in 1234"
}
}
}
it "displays that the code was sent" do
expect(env.ui)
.to receive(:success)
.with("2FA code sent to SMS number ending in 1234.")
stub_request(:post, "#{Vagrant.server_url}/api/v1/two-factor/request-code").
with(body: JSON.dump(request), headers: headers).
to_return(status: 201, body: JSON.dump(response))
client.username_or_email = login
client.password = password
client.request_code delivery_method
end
end
describe "#token" do
it "reads ATLAS_TOKEN" do
stub_env("ATLAS_TOKEN" => "ABCD1234")
expect(subject.token).to eq("ABCD1234")
end
it "reads the stored file" do
subject.store_token("EFGH5678")
expect(subject.token).to eq("EFGH5678")
end
it "prefers the environment variable" do
stub_env("VAGRANT_CLOUD_TOKEN" => "ABCD1234")
subject.store_token("EFGH5678")
expect(subject.token).to eq("ABCD1234")
end
it "prints a warning if the envvar and stored file are both present" do
stub_env("VAGRANT_CLOUD_TOKEN" => "ABCD1234")
subject.store_token("EFGH5678")
expect(env.ui).to receive(:warn).with(/detected both/)
subject.token
end
it "returns nil if there's no token set" do
expect(subject.token).to be(nil)
end
end
describe "#store_token, #clear_token" do
it "stores the token and can re-access it" do
subject.store_token("foo")
expect(subject.token).to eq("foo")
expect(described_class.new(env).token).to eq("foo")
end
it "deletes the token" do
subject.store_token("foo")
subject.clear_token
expect(subject.token).to be_nil
end
end
end

View File

@ -0,0 +1,24 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/list")
describe VagrantPlugins::CloudCommand::Command::List do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
end
end

View File

@ -0,0 +1,85 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/provider/create")
describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Create do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
let(:provider) { double("provider") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(VagrantCloud::Version).to receive(:new)
.with(box, "1.0.0", nil, nil, client.token)
.and_return(version)
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0"] }
it "creates a provider" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token).
and_return(provider)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(iso_env.ui).to receive(:warn)
expect(provider).to receive(:create_provider).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token).
and_return(provider)
allow(provider).to receive(:create_provider).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 422))
expect(subject.execute).to eq(1)
end
end
context "with arguments and a remote url" do
let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0", "https://box.com/box"] }
it "creates a provider" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, "https://box.com/box", "vagrant", "box-name", client.token).
and_return(provider)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(iso_env.ui).not_to receive(:warn)
expect(provider).to receive(:create_provider).and_return({})
expect(subject.execute).to eq(0)
end
end
end

View File

@ -0,0 +1,70 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/provider/delete")
describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Delete do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
let(:provider) { double("provider") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(VagrantCloud::Version).to receive(:new)
.with(box, "1.0.0", nil, nil, client.token)
.and_return(version)
allow(iso_env.ui).to receive(:ask).
and_return("y")
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0"] }
it "deletes a provider" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, nil, nil, client.token).
and_return(provider)
expect(provider).to receive(:delete).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, nil, nil, client.token).
and_return(provider)
allow(provider).to receive(:delete).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,85 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/provider/update")
describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Update do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box", create: true, read: {}) }
let(:version) { double("version", create_version: true, release: true) }
let(:provider) { double("provider", create_provider: true, upload_file: true) }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(VagrantCloud::Version).to receive(:new)
.with(box, "1.0.0", nil, nil, client.token)
.and_return(version)
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0"] }
it "updates a provider" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token).
and_return(provider)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(iso_env.ui).to receive(:warn)
expect(provider).to receive(:update).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token).
and_return(provider)
allow(provider).to receive(:update).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
context "with arguments and a remote url" do
let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0", "https://box.com/box"] }
it "creates a provider" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, "https://box.com/box", "vagrant", "box-name", client.token).
and_return(provider)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(iso_env.ui).not_to receive(:warn)
expect(provider).to receive(:update).and_return({})
expect(subject.execute).to eq(0)
end
end
end

View File

@ -0,0 +1,79 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/provider/upload")
describe VagrantPlugins::CloudCommand::ProviderCommand::Command::Upload do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
let(:provider) { double("provider") }
let(:uploader) { double("uploader") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(VagrantCloud::Version).to receive(:new)
.with(box, "1.0.0", nil, nil, client.token)
.and_return(version)
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "virtualbox", "1.0.0", "path/to/box.box"] }
it "uploads a provider" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token).
and_return(provider)
allow(provider).to receive(:upload_url).
and_return("http://upload.here/there")
allow(Vagrant::Util::Uploader).to receive(:new).
with("http://upload.here/there", "path/to/box.box", {ui: anything}).
and_return(uploader)
expect(uploader).to receive(:upload!)
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Provider).to receive(:new).
with(version, "virtualbox", nil, nil, "vagrant", "box-name", client.token).
and_return(provider)
allow(provider).to receive(:upload_url).
and_return("http://upload.here/there")
allow(Vagrant::Util::Uploader).to receive(:new).
with("http://upload.here/there", "path/to/box.box", {ui: anything}).
and_return(uploader)
allow(uploader).to receive(:upload!).
and_raise(Vagrant::Errors::UploaderError.new(exit_code: 1, message: "Error"))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,111 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/publish")
describe VagrantPlugins::CloudCommand::Command::Publish do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
let(:client) { double("client", token: "1234token1234") }
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:box_path) { "path/to/the/virtualbox.box" }
let(:box) { double("box", create: true, read: {}) }
let(:version) { double("version", create_version: true, release: true) }
let(:provider) { double("provider", create_provider: true, upload_file: true) }
let(:uploader) { double("uploader") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(iso_env.ui).to receive(:ask).
and_return("y")
allow(VagrantCloud::Box).to receive(:new).and_return(box)
allow(VagrantCloud::Version).to receive(:new).and_return(version)
allow(VagrantCloud::Provider).to receive(:new).and_return(provider)
allow(File).to receive(:absolute_path).and_return("/full/#{box_path}")
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let(:argv) { ["vagrant/box", "1.0.0", "virtualbox", box_path] }
it "publishes a box given options" do
allow(provider).to receive(:upload_url).and_return("http://upload.here/there")
allow(Vagrant::Util::Uploader).to receive(:new).
with("http://upload.here/there", "/full/path/to/the/virtualbox.box", {ui: anything}).
and_return(uploader)
allow(uploader).to receive(:upload!)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(subject.execute).to eq(0)
end
it "catches a ClientError if something goes wrong" do
allow(provider).to receive(:upload_url).and_return("http://upload.here/there")
allow(Vagrant::Util::Uploader).to receive(:new).
with("http://upload.here/there", "/full/path/to/the/virtualbox.box", {ui: anything}).
and_return(uploader)
allow(uploader).to receive(:upload!)
allow(box).to receive(:create).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
it "calls update if entity already exists" do
allow(provider).to receive(:upload_url).and_return("http://upload.here/there")
allow(Vagrant::Util::Uploader).to receive(:new).
with("http://upload.here/there", "/full/path/to/the/virtualbox.box", {ui: anything}).
and_return(uploader)
allow(uploader).to receive(:upload!)
allow(box).to receive(:create).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 422))
expect(box).to receive(:update)
expect(subject.execute).to eq(0)
end
end
context "with arguments and releasing a box" do
let(:argv) { ["vagrant/box", "1.0.0", "virtualbox", box_path, "--release"] }
it "releases the box" do
allow(provider).to receive(:upload_url).and_return("http://upload.here/there")
allow(Vagrant::Util::Uploader).to receive(:new).
with("http://upload.here/there", "/full/path/to/the/virtualbox.box", {ui: anything}).
and_return(uploader)
allow(uploader).to receive(:upload!)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(version).to receive(:release)
expect(subject.execute).to eq(0)
end
end
context "with arguments and a remote url" do
let(:argv) { ["vagrant/box", "1.0.0", "virtualbox", "--url", "https://www.boxes.com/path/to/the/virtualbox.box"] }
it "does not upload a file" do
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(subject.execute).to eq(0)
expect(provider).not_to receive(:upload_file)
end
end
end

View File

@ -0,0 +1,77 @@
require File.expand_path("../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/search")
describe VagrantPlugins::CloudCommand::Command::Search do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
let(:client) { double("client", token: "1234token1234") }
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_search_results).
and_return(true)
end
context "with no arguments" do
let (:search) { double("search", search: {"boxes"=>["all of them"]}) }
it "makes a request to search all boxes and formats them" do
allow(VagrantCloud::Search).to receive(:new).
and_return(search)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_search_results)
expect(subject.execute).to eq(0)
end
end
context "with no arguments and an error occurs making requests" do
let (:search) { double("search") }
it "catches a ClientError if something goes wrong" do
allow(VagrantCloud::Search).to receive(:new).
and_return(search)
allow(search).to receive(:search).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
context "with no arguments and no results" do
let (:search) { double("search", search: {"boxes"=>[]}) }
it "makes a request to search all boxes and formats them" do
allow(VagrantCloud::Search).to receive(:new).
and_return(search)
expect(VagrantPlugins::CloudCommand::Util).not_to receive(:format_search_results)
subject.execute
end
end
context "with arguments" do
let (:search) { double("search", search: {"boxes"=>["all of them"]}) }
let (:argv) { ["ubuntu", "--page", "1", "--order", "desc", "--limit", "100", "--provider", "provider", "--sort", "downloads"] }
it "sends the options to make a request with" do
allow(VagrantCloud::Search).to receive(:new).
and_return(search)
expect(search).to receive(:search).
with("ubuntu", "provider", "downloads", "desc", 100, 1)
subject.execute
end
end
end

View File

@ -0,0 +1,65 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/version/create")
describe VagrantPlugins::CloudCommand::VersionCommand::Command::Create do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "1.0.0", "-d", "description"] }
it "creates a version" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, "description", client.token).
and_return(version)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(version).to receive(:create_version).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, "description", client.token).
and_return(version)
allow(version).to receive(:create_version).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 422))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,66 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/version/delete")
describe VagrantPlugins::CloudCommand::VersionCommand::Command::Delete do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(iso_env.ui).to receive(:ask).
and_return("y")
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "1.0.0"] }
it "deletes a version" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, nil, client.token).
and_return(version)
expect(version).to receive(:delete).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, nil, client.token).
and_return(version)
allow(version).to receive(:delete).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,66 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/version/release")
describe VagrantPlugins::CloudCommand::VersionCommand::Command::Release do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(iso_env.ui).to receive(:ask).
and_return("y")
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "1.0.0"] }
it "releases a version" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, nil, client.token).
and_return(version)
expect(version).to receive(:release).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, nil, client.token).
and_return(version)
allow(version).to receive(:release).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,66 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/version/revoke")
describe VagrantPlugins::CloudCommand::VersionCommand::Command::Revoke do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
allow(iso_env.ui).to receive(:ask).
and_return("y")
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "1.0.0"] }
it "revokes a version" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, nil, client.token).
and_return(version)
expect(version).to receive(:revoke).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, nil, client.token).
and_return(version)
expect(version).to receive(:revoke).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,65 @@
require File.expand_path("../../../../../base", __FILE__)
require Vagrant.source_root.join("plugins/commands/cloud/version/update")
describe VagrantPlugins::CloudCommand::VersionCommand::Command::Update do
include_context "unit"
let(:argv) { [] }
let(:iso_env) do
# We have to create a Vagrantfile so there is a root path
env = isolated_environment
env.vagrantfile("")
env.create_vagrant_env
end
subject { described_class.new(argv, iso_env) }
let(:action_runner) { double("action_runner") }
let(:client) { double("client", token: "1234token1234") }
let(:box) { double("box") }
let(:version) { double("version") }
before do
allow(iso_env).to receive(:action_runner).and_return(action_runner)
allow(VagrantPlugins::CloudCommand::Util).to receive(:client_login).
and_return(client)
allow(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results).
and_return(true)
allow(VagrantCloud::Box).to receive(:new)
.with(anything, "box-name", nil, nil, nil, client.token)
.and_return(box)
end
context "with no arguments" do
it "shows help" do
expect { subject.execute }.
to raise_error(Vagrant::Errors::CLIInvalidUsage)
end
end
context "with arguments" do
let (:argv) { ["vagrant/box-name", "1.0.0", "-d", "description"] }
it "updates a version" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, "description", client.token).
and_return(version)
expect(VagrantPlugins::CloudCommand::Util).to receive(:format_box_results)
expect(version).to receive(:update).and_return({})
expect(subject.execute).to eq(0)
end
it "displays an error if encoutering a problem with the request" do
allow(VagrantCloud::Version).to receive(:new).
with(box, "1.0.0", nil, "description", client.token).
and_return(version)
allow(version).to receive(:update).
and_raise(VagrantCloud::ClientError.new("Fail Message", "Message", 404))
expect(subject.execute).to eq(1)
end
end
end

View File

@ -0,0 +1,6 @@
require File.expand_path("../../../base", __FILE__)
require "vagrant/util/curl_helper"
describe Vagrant::Util::CurlHelper do
end

View File

@ -0,0 +1,50 @@
require File.expand_path("../../../base", __FILE__)
require "vagrant/util/uploader"
describe Vagrant::Util::Uploader do
let(:destination) { "fake" }
let(:file) { "my/file.box" }
let(:curl_options) { [destination, "--request", "PUT", "--upload-file", file, {notify: :stderr}] }
let(:subprocess_result) do
double("subprocess_result").tap do |result|
allow(result).to receive(:exit_code).and_return(exit_code)
allow(result).to receive(:stderr).and_return("")
end
end
subject { described_class.new(destination, file, options) }
before :each do
allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(subprocess_result)
end
describe "#upload!" do
context "with a good exit status" do
let(:options) { {} }
let(:exit_code) { 0 }
it "uploads the file and returns true" do
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("curl", *curl_options).
and_return(subprocess_result)
expect(subject.upload!).to be
end
end
context "with a bad exit status" do
let(:options) { {} }
let(:exit_code) { 1 }
it "raises an exception" do
expect(Vagrant::Util::Subprocess).to receive(:execute).
with("curl", *curl_options).
and_return(subprocess_result)
expect { subject.upload! }.
to raise_error(Vagrant::Errors::UploaderError)
end
end
end
end

View File

@ -32,6 +32,7 @@ Gem::Specification.new do |s|
s.add_dependency "winrm", "~> 2.1"
s.add_dependency "winrm-fs", "~> 1.0"
s.add_dependency "winrm-elevated", "~> 1.1"
s.add_dependency "vagrant_cloud", "~> 2.0.0"
# NOTE: The ruby_dep gem is an implicit dependency from the listen gem. Later versions
# of the ruby_dep gem impose an aggressive constraint on the required ruby version (>= 2.2.5).

View File

@ -0,0 +1,339 @@
---
layout: "docs"
page_title: "vagrant cloud - Command-Line Interface"
sidebar_current: "cli-cloud"
description: |-
The "vagrant cloud" command can be used for taking actions against
Vagrant Cloud like searching or uploading a Vagrant Box
---
# Cloud
**Command: `vagrant cloud`**
This is the command used to manage anything related to [Vagrant Cloud](https://vagrantcloud.com).
The main functionality of this command is exposed via subcommands:
* [`auth`](#cloud-auth)
* [`box`](#cloud-box)
* [`provider`](#cloud-provider)
* [`publish`](#cloud-publish)
* [`search`](#cloud-search)
* [`version`](#cloud-version)
# Cloud Auth
**Command: `vagrant cloud auth`**
The `cloud auth` command is for handling all things related to authorization with
Vagrant Cloud.
* [`login`](#cloud-auth-login)
* [`logout`](#cloud-auth-logout)
* [`whoami`](#cloud-auth-whoami)
## Cloud Auth Login
**Command: `vagrant cloud auth login`**
The login command is used to authenticate with [HashiCorp's Vagrant Cloud](/docs/vagrant-cloud)
server. Logging in is only necessary if you are accessing protected boxes.
**Logging in is not a requirement to use Vagrant.** The vast majority
of Vagrant does _not_ require a login. Only certain features such as protected
boxes.
The reference of available command-line flags to this command
is available below.
### Options
* `--check` - This will check if you are logged in. In addition to outputting
whether you are logged in or not, the command exit status will be 0 if you are
logged in, or 1 if you are not.
* `--logout` - This will log you out if you are logged in. If you are already
logged out, this command will do nothing. It is not an error to call this
command if you are already logged out.
* `--token` - This will set the Vagrant Cloud login token manually to the provided
string. It is assumed this token is a valid Vagrant Cloud access token.
### Examples
Securely authenticate to Vagrant Cloud using a username and password:
```text
$ vagrant cloud auth login
# ...
Vagrant Cloud username:
Vagrant Cloud password:
```
Check if the current user is authenticated:
```text
$ vagrant cloud auth login --check
You are already logged in.
```
Securely authenticate with Vagrant Cloud using a token:
```text
$ vagrant cloud auth login --token ABCD1234
The token was successfully saved.
```
## Cloud Auth Logout
**Command: `vagrant cloud auth logout`**
This will log you out if you are logged in. If you are already
logged out, this command will do nothing. It is not an error to call this
command if you are already logged out.
## Cloud Auth Whomi
**Command: `vagrant cloud auth whoami [TOKEN]`**
This command will validate your Vagrant Cloud token and will print the user who
it belongs to. If a token is passed in, it will attempt to validate it instead
of the token stored stored on disk.
# Cloud Box
**Command: `vagrant cloud box`**
The `cloud box` command is used to manage life cycle operations for all `box`
entities on Vagrant Cloud.
* [`create`](#cloud-box-create)
* [`delete`](#cloud-box-delete)
* [`show`](#cloud-box-show)
* [`update`](#cloud-box-update)
## Cloud Box Create
**Command: `vagrant cloud box create ORGANIZATION/BOX-NAME`**
The box create command is used to create a new box entry on Vagrant Cloud.
### Options
* `--description DESCRIPTION` - A full description of the box. Can be
formatted with Markdown.
* `--short-description DESCRIPTION` - A short summary of the box.
* `--private` - Will make the new box private (Public by default)
## Cloud Box Delete
**Command: `vagrant cloud box delete ORGANIZATION/BOX-NAME`**
The box delete command will _permanently_ delete the given box entry on Vagrant Cloud. Before
making the request, it will ask if you are sure you want to delete the box.
## Cloud Box Show
**Command: `vagrant cloud box show ORGANIZATION/BOX-NAME`**
The box show command will display information about the latest version for the given Vagrant box.
## Cloud Box Update
**Command: `vagrant cloud box update ORGANIZATION/BOX-NAME`**
The box update command will update an already created box on Vagrant Cloud with the given options.
### Options
* `--description DESCRIPTION` - A full description of the box. Can be
formatted with Markdown.
* `--short-description DESCRIPTION` - A short summary of the box.
* `--private` - Will make the new box private (Public by default)
# Cloud Provider
**Command: `vagrant cloud provider`**
The `cloud provider` command is used to manage the life cycle operations for all
`provider` entities on Vagrant Cloud.
* [`create`](#cloud-provider-create)
* [`delete`](#cloud-provider-delete)
* [`update`](#cloud-provider-update)
* [`upload`](#cloud-provider-upload)
## Cloud Provider Create
**Command: `vagrant cloud provider create ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION [URL]`**
The provider create command is used to create a new provider entry on Vagrant Cloud.
The `url` argument is expected to be a remote URL that Vagrant Cloud can use
to download the provider. If no `url` is specified, the provider entry can be updated
later with a url or the [upload](#cloud-provider-upload) command can be used to
upload a Vagrant [box file](/docs/boxes.html).
## Cloud Provider Delete
**Command: `vagrant cloud provider delete ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION`**
The provider delete command is used to delete a provider entry on Vagrant Cloud.
Before making the request, it will ask if you are sure you want to delete the provider.
## Cloud Provider Update
**Command: `vagrant cloud provider update ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION [URL]`**
The provider update command will update an already created provider for a box on
Vagrant Cloud with the given options.
## Cloud Provider Upload
**Command: `vagrant cloud provider upload ORGANIZATION/BOX-NAME PROVIDER-NAME VERSION BOX-FILE`**
The provider upload command will upload a Vagrant [box file](/docs/boxes.html) to Vagrant Cloud for
the specified version and provider.
# Cloud Publish
**Command: `vagrant cloud publish ORGANIZATION/BOX-NAME VERSION PROVIDER-NAME [PROVIDER-FILE]`**
The publish command is a complete solution for creating and updating a
Vagrant box on Vagrant Cloud. Instead of having to create each attribute of a Vagrant
box with separate commands, the publish command instead asks you to provide all
the information required before creating or updating a new box.
## Options
* `--box-version VERSION` - Version to create for the box
* `--description DESCRIPTION` - A full description of the box. Can be
formatted with Markdown.
* `--force` - Disables confirmation when creating or updating a box.
* `--short-description DESCRIPTION` - A short summary of the box.
* `--private` - Will make the new box private (Public by default)
* `--release` - Automatically releases the box after creation (Unreleased by default)
* `--url` - Valid remote URL to download the box file
* `--version-description DESCRIPTION` - Description of the version that will be created.
## Examples
Creating a new box on Vagrant Cloud:
```text
$ vagrant cloud publish briancain/supertest 1.0.0 virtualbox boxes/my/virtualbox.box -d "A really cool box to download and use" --version-description "A cool version" --release --short-description "Donwload me!"
You are about to create a box on Vagrant Cloud with the following options:
briancain/supertest (1.0.0) for virtualbox
Automatic Release: true
Box Description: A really cool box to download and use
Box Short Description: Download me!
Version Description: A cool version
Do you wish to continue? [y/N] y
Creating a box entry...
Creating a version entry...
Creating a provider entry...
Uploading provider with file /Users/vagrant/boxes/my/virtualbox.box
Releasing box...
Complete! Published briancain/supertest
tag: briancain/supertest
username: briancain
name: supertest
private: false
downloads: 0
created_at: 2018-07-25T17:53:04.340Z
updated_at: 2018-07-25T18:01:10.665Z
short_description: Download me!
description_markdown: A reall cool box to download and use
current_version: 1.0.0
providers: virtualbox
```
# Cloud Search
**Command: `vagrant cloud search QUERY`**
The cloud search command will take a query and search Vagrant Cloud for any matching
Vagrant boxes. Various filters can be applied to the results.
## Options
* `--json` - Format search results in JSON.
* `--page PAGE` - The page to display. Defaults to the first page of results.
* `--short` - Shows a simple list of box names for the results.
* `--order ORDER` - Order to display results. Can either be `desc` or `asc`.
Defaults to `desc`.
* `--limit LIMIT` - Max number of search results to display. Defaults to 25.
* `--provider PROVIDER` - Filter search results to a single provider.
* `--sort-by SORT` - The field to sort results on. Can be `created`, `downloads`
, or `updated`. Defaults to `downloads`.
## Examples
If you are looking for a HashiCorp box:
```text
vagrant cloud search hashicorp --limit 5
| NAME | VERSION | DOWNLOADS | PROVIDERS |
+-------------------------+---------+-----------+---------------------------------+
| hashicorp/precise64 | 1.1.0 | 6,675,725 | virtualbox,vmware_fusion,hyperv |
| hashicorp/precise32 | 1.0.0 | 2,261,377 | virtualbox |
| hashicorp/boot2docker | 1.7.8 | 59,284 | vmware_desktop,virtualbox |
| hashicorp/connect-vm | 0.1.0 | 6,912 | vmware_desktop,virtualbox |
| hashicorp/vagrant-share | 0.1.0 | 3,488 | vmware_desktop,virtualbox |
+-------------------------+---------+-----------+---------------------------------+
```
# Cloud Version
**Command: `vagrant cloud version`**
The `cloud version` command is used to manage life cycle operations for all `version`
entities for a box on Vagrant Cloud.
* [`create`](#cloud-version-create)
* [`delete`](#cloud-version-delete)
* [`release`](#cloud-version-release)
* [`revoke`](#cloud-version-revoke)
* [`update`](#cloud-version-update)
## Cloud Version Create
**Command: `vagrant cloud version create ORGANIZATION/BOX-NAME VERSION`**
The cloud create command creates a version entry for a box on Vagrant Cloud.
### Options
* `--description DESCRIPTION` - Description of the version that will be created.
## Cloud Version Delete
**Command: `vagrant cloud version delete ORGANIZATION/BOX-NAME VERSION`**
The cloud delete command deletes a version entry for a box on Vagrant Cloud.
Before making the request, it will ask if you are sure you want to delete the version.
## Cloud Version Release
**Command: `vagrant cloud version release ORGANIZATION/BOX-NAME VERSION`**
The cloud release command releases a version entry for a box on Vagrant Cloud
if it already exists. Before making the request, it will ask if you are sure you
want to release the version.
## Cloud Version Revoke
**Command: `vagrant cloud version revoke ORGANIZATION/BOX-NAME VERSION`**
The cloud revoke command revokes a version entry for a box on Vagrant Cloud
if it already exists. Before making the request, it will ask if you are sure you
want to revoke the version.
## Cloud Version Update
**Command: `vagrant cloud version update ORGANIZATION/BOX-NAME VERSION`**
### Options
* `--description DESCRIPTION` - Description of the version that will be created.

View File

@ -20,6 +20,7 @@
<a href="/docs/cli/">Commands (CLI)</a>
<ul class="nav">
<li<%= sidebar_current("cli-box") %>><a href="/docs/cli/box.html">box</a></li>
<li<%= sidebar_current("cli-cloud") %>><a href="/docs/cli/cloud.html">cloud</a></li>
<li<%= sidebar_current("cli-connect") %>><a href="/docs/cli/connect.html">connect</a></li>
<li<%= sidebar_current("cli-destroy") %>><a href="/docs/cli/destroy.html">destroy</a></li>
<li<%= sidebar_current("cli-globalstatus") %>><a href="/docs/cli/global-status.html">global-status</a></li>